001: /*****************************************************************************
002: * TextCompiler.java
003: * ****************************************************************************/package org.openlaszlo.compiler;
004:
005: import org.openlaszlo.utils.ChainedException;
006: import java.io.*;
007: import java.util.*;
008: import org.jdom.Attribute;
009: import org.jdom.Comment;
010: import org.jdom.CDATA;
011: import org.jdom.Document;
012: import org.jdom.Element;
013: import org.jdom.EntityRef;
014: import org.jdom.Text;
015: import org.jdom.output.XMLOutputter;
016: import java.util.Iterator;
017: import org.apache.log4j.*;
018: import org.openlaszlo.iv.flash.api.text.Font;
019: import java.awt.geom.Rectangle2D;
020:
021: /** Utility functions for measuring HTML content, and translating it into Flash strings.
022: *
023: * @author <a href="mailto:hminsky@laszlosystems.com">Henry Minsky</a>
024: */
025: abstract class TextCompiler {
026:
027: private static Logger mLogger = Logger
028: .getLogger(TextCompiler.class);
029: private static Logger mTextLogger = Logger.getLogger("lps.text");
030:
031: public static double computeTextWidth(String text,
032: FontInfo fontInfo, SWFWriter generator)
033: throws CompilationError {
034: LineMetrics lm = new LineMetrics();
035: return computeTextWidth(text, fontInfo, generator, lm);
036: }
037:
038: /** Check if a specified font is known by the Font Manager
039: *
040: * @param generator
041: * @param fontInfo the font spec you want to check
042: *
043: * This will throw an informative CompilationError if the font does not exist.
044: */
045: public static void checkFontExists(SWFWriter generator,
046: FontInfo fontInfo) {
047: String fontName = fontInfo.getName();
048: int size = fontInfo.getSize();
049: int style = fontInfo.styleBits;
050:
051: if (!generator.checkFontExists(fontInfo)) {
052: throw new CompilationError(
053: /* (non-Javadoc)
054: * @i18n.test
055: * @org-mes="Can't find font " + p[0] + " of style " + p[1]
056: */
057: org.openlaszlo.i18n.LaszloMessages.getMessage(
058: TextCompiler.class.getName(), "051018-60",
059: new Object[] { fontName, fontInfo.getStyle() }));
060: }
061: }
062:
063: /**
064: * Compute text width for a given font
065: *
066: * @param text text stringtext string
067: * @param fontInfo font info for this text
068: * @return text width in pixels
069: */
070: public static double computeTextWidth(String text,
071: FontInfo fontInfo, SWFWriter generator, LineMetrics lm)
072: throws CompilationError {
073:
074: boolean trace = false; //mProperties.getProperty("trace.fonts", "false") == "true";
075:
076: String fontName = fontInfo.getName();
077: int size = fontInfo.getSize();
078: int style = fontInfo.styleBits;
079:
080: mTextLogger.debug(
081: /* (non-Javadoc)
082: * @i18n.test
083: * @org-mes="computeTextWidth fontName " + p[0] + " (style: " + p[1] + ", size: " + p[2] + ") text: " + p[3]
084: */
085: org.openlaszlo.i18n.LaszloMessages.getMessage(
086: TextCompiler.class.getName(), "051018-87",
087: new Object[] { fontName, fontInfo.getStyle(),
088: new Integer(fontInfo.getSize()), text }));
089:
090: if (text.length() == 0) {
091: return 0;
092: }
093:
094: generator.checkFontExists(fontInfo);
095:
096: FontFamily family = generator.getFontManager().getFontFamily(
097: fontName);
098: if (family == null) {
099: throw new CompilationError("Can't find font " + fontName);
100: }
101:
102: Font font = family.getStyle(style);
103: if (font == null) {
104: throw new CompilationError(
105: /* (non-Javadoc)
106: * @i18n.test
107: * @org-mes="Can't measure text because font " + p[0] + " " + p[1] + " is missing."
108: */
109: org.openlaszlo.i18n.LaszloMessages.getMessage(
110: TextCompiler.class.getName(), "051018-109",
111: new Object[] { FontInfo.styleBitsToString(style),
112: fontName }));
113: }
114:
115: Rectangle2D[] bounds = family.getBounds(style);
116:
117: if (bounds == null) {
118: throw new CompilationError(
119: /* (non-Javadoc)
120: * @i18n.test
121: * @org-mes="Can't measure text because font " + p[0] + " " + p[1] + " is missing its bounds array."
122: */
123: org.openlaszlo.i18n.LaszloMessages.getMessage(
124: TextCompiler.class.getName(), "051018-122",
125: new Object[] { FontInfo.styleBitsToString(style),
126: fontName }));
127: }
128:
129: double width = 0;
130: int length = text.length();
131: char c = text.charAt(0);
132: int idx = font.getIndex(c);
133: int nextIdx;
134:
135: double last_charwidth = 0;
136:
137: // Cope with \n \r and missing characters? XXX
138: for (int i = 0; i < length; i++) {
139: if (idx == -1) {
140: mLogger.warn(
141: /* (non-Javadoc)
142: * @i18n.test
143: * @org-mes="Character \'" + p[0] + "\' (" + p[1] + ") not available in font " + p[2] + " (style " + p[3] + ")"
144: */
145: org.openlaszlo.i18n.LaszloMessages.getMessage(
146: TextCompiler.class.getName(), "051018-143",
147: new Object[] { new Character(c),
148: new Integer((int) c), fontName,
149: fontInfo.getStyle() }));
150: continue;
151: } else {
152: double adv = font.getAdvanceValue(idx);
153:
154: if (i == length - 1) {
155: double m = 0;
156: try {
157: m = bounds[idx].getMaxX();
158: } catch (Exception e) {
159: }
160: if (m > adv) {
161: adv = m;
162: }
163: }
164:
165: if (i == 0) {
166: try {
167: double m = bounds[idx].getMinX();
168: if (m > 0) {
169: adv += m;
170: }
171: } catch (Exception e) {
172: }
173: }
174:
175: last_charwidth = adv;
176: width += adv;
177:
178: mLogger.debug("adv " + adv);
179: }
180:
181: if (i != length - 1) {
182: c = text.charAt(i + 1);
183: nextIdx = font.getIndex(c);
184: if (nextIdx != -1) {
185: double cw = font.getKerning(idx, nextIdx);
186: width += cw;
187: }
188: idx = nextIdx;
189: }
190: }
191: // Width in pixels
192: double w = (double) (width * fontInfo.getSize()) / 1024.0;
193:
194: // If the last character was a space, remember it's width, as we may need
195: // to trim the trailing space from the HTML formatted text
196: if (c == ' ') {
197: lm.last_spacewidth = (double) (last_charwidth * fontInfo
198: .getSize()) / 1024.0;
199: }
200:
201: mTextLogger.debug(
202: /* (non-Javadoc)
203: * @i18n.test
204: * @org-mes="computeTextWidth: " + p[0] + " (font: " + p[1] + ", size: " + p[2] + ", style: " + p[3] + ") has textwidth: " + p[4]
205: */
206: org.openlaszlo.i18n.LaszloMessages.getMessage(
207: TextCompiler.class.getName(), "051018-201",
208: new Object[] { text, fontInfo.getName(),
209: new Integer(fontInfo.getSize()),
210: fontInfo.getStyle(), new Double(w) }));
211:
212: // FIXME: [2003-09-26 bloch] handle empty string case? should it be w/out slop?
213: // Match this in LzNewText.as
214: //
215: final int SLOP = 2;
216:
217: return w + SLOP;
218: }
219:
220: /** Compute the text width of a string. If there are multiple
221: * lines, return the maximum line width.
222: *
223: * <p>
224: *
225: * The only multi-line strings we will ever see here will be
226: * non-normalized text such as inside <pre;> verbatim
227: * regions, because in normal running HTML text, the normalization
228: * will have stripped out newlines.
229: *
230: * <p>
231: *
232: * The LineMetrics holds state from possibly a previous text run
233: * on the same line, telling us whether we need to prepend an
234: * extra whitespace.
235: */
236: static double getTextWidth(String str, FontInfo fontInfo,
237: SWFWriter generator, LineMetrics lm) {
238:
239: double maxw = 0;
240: int lastpos = 0;
241: int nextpos = str.indexOf('\n');
242: String substr;
243:
244: if (nextpos < 0) {
245: return computeTextWidth(str, fontInfo, generator, lm);
246: }
247: while (nextpos >= 0) {
248: substr = str.substring(lastpos, nextpos);
249: maxw = Math.max(maxw, computeTextWidth(substr, fontInfo,
250: generator, lm));
251: lastpos = nextpos + 1;
252: nextpos = str.indexOf('\n', lastpos);
253: lm.nlines++;
254: }
255:
256: substr = str.substring(lastpos);
257: maxw = Math.max(maxw, computeTextWidth(substr, fontInfo,
258: generator, lm));
259: return maxw;
260: }
261:
262: /** Measure the content text allowing for "HTML" markup.
263: *
264: * This uses rules similar to how you would measure browser HTML text:
265: *
266: * <ul>
267: * <li> All text is whitespace normalized, except that which occurs between <pre> tags
268: * <li> Linebreaks occur only when <br/> or <p/> elements occur, or when a newline
269: * is present inside of a <pre> region.
270: * <li> When multiple text lines are present, the length of the longest line is returned.
271: * </ul>
272: */
273:
274: static LineMetrics getElementWidth(Element e, FontInfo fontInfo,
275: SWFWriter generator) {
276: LineMetrics lm = new LineMetrics();
277: getElementWidth(e, fontInfo, generator, lm);
278: lm.endOfLine();
279: // cache the normalized HTML content
280: ((ElementWithLocationInfo) e).setHTMLContent(lm.getText());
281: return lm;
282: }
283:
284: /** Gets the text content, with HTML normalization rules applied */
285: static String getHTMLContent(Element e) {
286: // check if the normalized text is cached
287: if ((e instanceof ElementWithLocationInfo)
288: && ((ElementWithLocationInfo) e).getHTMLContent() != null) {
289: return ((ElementWithLocationInfo) e).getHTMLContent();
290: }
291:
292: LineMetrics lm = new LineMetrics();
293: // Just use a dummy font info, we only care about the HTML
294: // text, not string widths
295: FontInfo fontInfo = new FontInfo("default", "8", "");
296: getElementWidth(e, fontInfo, null, lm);
297: lm.endOfLine();
298: return lm.getText();
299: }
300:
301: /** Return text suitable for passing to Laszlo inputtext component.
302: * This means currently no HTML tags are supported except PRE
303: */
304: static String getInputText(Element e) {
305: String text = "";
306: for (Iterator iter = e.getContent().iterator(); iter.hasNext();) {
307: Object node = iter.next();
308: if (node instanceof Element) {
309: Element child = (Element) node;
310: String tagName = child.getName();
311: if (tagName.equals("p") || tagName.equals("br")) {
312: text += "\n";
313: } else if (tagName.equals("pre")) {
314: text += child.getText();
315: } else {
316: // ignore everything else
317: }
318: } else if ((node instanceof Text)
319: || (node instanceof CDATA)) {
320: if (node instanceof Text) {
321: text += ((Text) node).getTextNormalize();
322: } else {
323: text += ((CDATA) node).getTextNormalize();
324: }
325: }
326: }
327: return text;
328: }
329:
330: /**
331: Processes the text content of the element. The element
332: content may contain XHTML markup elements, which we will
333: interpret as we map over the content. Normally, whitespace
334: will be normalized away. However, preformat <pre> tags
335: will cause the enclosed text to be treated as verbatim,
336: meaning means that whitespace and linebreaks will be
337: preserved.
338:
339: Supported XHTML markup is currently:
340: <ul>
341: <li> P, BR cause linebreaks
342: <li> PRE sets verbatim (literal whitespace) mode
343: <li> font face control: B, I, FONT tags modify the font
344: <li> A [href] indicates a hyperlink
345: </ul>
346:
347: */
348: static void getElementWidth(Element e, FontInfo fontInfo,
349: SWFWriter generator, LineMetrics lm) {
350: for (Iterator iter = e.getContent().iterator(); iter.hasNext();) {
351: Object node = iter.next();
352: if (node instanceof Element) {
353: Element child = (Element) node;
354: String tagName = child.getName();
355:
356: if (tagName.equals("br")) {
357: lm.newline(); // explicit linebreak
358: getElementWidth(child, fontInfo, generator, lm);
359: if (!child.getText().equals("")) {
360: lm.newline();
361: }
362: } else if (tagName.equals("p")) {
363: lm.paragraphBreak();
364: getElementWidth(child, fontInfo, generator, lm);
365: lm.paragraphBreak();
366: } else if (tagName.equals("pre")) {
367: boolean prev = lm.verbatim;
368: lm.setVerbatim(true);
369: getElementWidth(child, fontInfo, generator, lm);
370: lm.setVerbatim(prev);
371: } else if (ViewSchema.isHTMLElement(child)) {
372: FontInfo newInfo = new FontInfo(fontInfo);
373: if (tagName.equals("b")) {
374: newInfo.styleBits |= FontInfo.BOLD;
375: } else if (tagName.equals("i")) {
376: newInfo.styleBits |= FontInfo.ITALIC;
377: } else if (tagName.equals("font")) {
378: ViewCompiler.setFontInfo(newInfo, child);
379: }
380: lm.addStartTag(tagName, newInfo, generator);
381: // print font-related attributes:
382: // face, size, color
383: // supported Flash HTML tags: http://www.macromedia.com/support/flash/ts/documents/htmltext.htm
384: for (Iterator attrs = child.getAttributes()
385: .iterator(); attrs.hasNext();) {
386: Attribute attr = (Attribute) attrs.next();
387: String name = attr.getName();
388: String value = child.getAttributeValue(name);
389: // TBD: [hqm nov-15-2002] The value ought to be quoted in case it contains double quotes
390: // (but no values of currently supported HTML tags will contain double quotes)
391: lm.addFormat(" " + name + "=\"" + value + "\"");
392: }
393: lm.endStartTag();
394: getElementWidth(child, newInfo, generator, lm);
395: lm.addEndTag(tagName);
396: }
397: } else if ((node instanceof Text)
398: || (node instanceof CDATA)) {
399: String rawtext;
400: if (node instanceof Text) {
401: rawtext = ((Text) node).getText();
402: } else {
403: rawtext = ((CDATA) node).getText();
404: }
405: if (lm.verbatim) {
406: lm.addSpan(rawtext, fontInfo, generator);
407: } else {
408: // Apply HTML normalization rules to the text content.
409: if (rawtext.length() > 0) {
410: // getTextNormalize turns an all-whitespace string into an empty string
411: String normalized_text;
412: if (node instanceof Text) {
413: normalized_text = ((Text) node)
414: .getTextNormalize();
415: } else {
416: normalized_text = ((CDATA) node)
417: .getTextNormalize();
418: }
419: lm.addHTML(rawtext, normalized_text, fontInfo,
420: generator);
421: }
422: }
423: } else if (node instanceof EntityRef) {
424: // EntityRefs don't seem to occur in our JDOM, they were all resolved
425: // to strings by the parser already
426: throw new RuntimeException(
427: /* (non-Javadoc)
428: * @i18n.test
429: * @org-mes="encountered unexpected EntityRef node in getElementWidth()"
430: */
431: org.openlaszlo.i18n.LaszloMessages.getMessage(
432: TextCompiler.class.getName(), "051018-418"));
433: }
434: }
435: }
436: }
|