| /******************************************************************************* |
| * Copyright (c) 2007, 2015 David Green and others. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v2.0 |
| * which accompanies this distribution, and is available at |
| * https://www.eclipse.org/legal/epl-2.0/ |
| * |
| * SPDX-License-Identifier: EPL-2.0 |
| * |
| * Contributors: |
| * David Green - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.mylyn.internal.wikitext.ui.viewer; |
| |
| import java.io.IOException; |
| import java.io.InputStreamReader; |
| import java.io.Reader; |
| import java.io.StringReader; |
| import java.nio.charset.StandardCharsets; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.IdentityHashMap; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.Stack; |
| |
| import javax.xml.parsers.ParserConfigurationException; |
| import javax.xml.parsers.SAXParser; |
| import javax.xml.parsers.SAXParserFactory; |
| |
| import org.eclipse.jface.text.Position; |
| import org.eclipse.jface.text.TextPresentation; |
| import org.eclipse.jface.text.source.Annotation; |
| import org.eclipse.jface.text.source.IAnnotationModel; |
| import org.eclipse.mylyn.internal.wikitext.ui.util.ImageCache; |
| import org.eclipse.mylyn.internal.wikitext.ui.viewer.annotation.BulletAnnotation; |
| import org.eclipse.mylyn.internal.wikitext.ui.viewer.annotation.HorizontalRuleAnnotation; |
| import org.eclipse.mylyn.internal.wikitext.ui.viewer.annotation.ImageAnnotation; |
| import org.eclipse.mylyn.wikitext.parser.css.CssParser; |
| import org.eclipse.mylyn.wikitext.parser.css.CssRule; |
| import org.eclipse.mylyn.wikitext.parser.css.ElementInfo; |
| import org.eclipse.mylyn.wikitext.parser.css.Stylesheet; |
| import org.eclipse.mylyn.wikitext.ui.annotation.AnchorHrefAnnotation; |
| import org.eclipse.mylyn.wikitext.ui.annotation.AnchorNameAnnotation; |
| import org.eclipse.mylyn.wikitext.ui.annotation.ClassAnnotation; |
| import org.eclipse.mylyn.wikitext.ui.annotation.IdAnnotation; |
| import org.eclipse.mylyn.wikitext.ui.annotation.TitleAnnotation; |
| import org.eclipse.mylyn.wikitext.util.IgnoreDtdEntityResolver; |
| import org.eclipse.swt.custom.StyleRange; |
| import org.eclipse.swt.graphics.Color; |
| import org.eclipse.swt.graphics.Font; |
| import org.eclipse.swt.graphics.GC; |
| import org.eclipse.swt.graphics.Point; |
| import org.xml.sax.Attributes; |
| import org.xml.sax.ContentHandler; |
| import org.xml.sax.InputSource; |
| import org.xml.sax.Locator; |
| import org.xml.sax.SAXException; |
| import org.xml.sax.XMLReader; |
| |
| /** |
| * Takes a valid XHTML document and converts the document into two distinct parts: |
| * <ol> |
| * <li>Text as it should be presented in a text viewer</li> |
| * <li>A {@link TextPresentation}</li> |
| * </ol> |
| * |
| * @author David Green |
| */ |
| public class HtmlTextPresentationParser { |
| /** |
| * Element names for spanning elements |
| */ |
| private static Set<String> spanElements = new HashSet<>(); |
| |
| /** |
| * element names for block elements |
| */ |
| private static Set<String> blockElements = new HashSet<>(); |
| |
| /** |
| * element names for elements that cause adjacent whitespace to be collapsed |
| */ |
| private static Set<String> whitespaceCollapsingElements = new HashSet<>(); |
| |
| private static Stylesheet defaultStylesheet; |
| |
| static { |
| spanElements.add("a"); //$NON-NLS-1$ |
| spanElements.add("abbr"); //$NON-NLS-1$ |
| spanElements.add("acronym"); //$NON-NLS-1$ |
| spanElements.add("b"); //$NON-NLS-1$ |
| spanElements.add("big"); //$NON-NLS-1$ |
| spanElements.add("blink"); //$NON-NLS-1$ |
| spanElements.add("cite"); //$NON-NLS-1$ |
| spanElements.add("code"); //$NON-NLS-1$ |
| spanElements.add("del"); //$NON-NLS-1$ |
| spanElements.add("dfn"); //$NON-NLS-1$ |
| spanElements.add("em"); //$NON-NLS-1$ |
| spanElements.add("font"); //$NON-NLS-1$ |
| spanElements.add("i"); //$NON-NLS-1$ |
| spanElements.add("img"); //$NON-NLS-1$ |
| spanElements.add("ins"); //$NON-NLS-1$ |
| spanElements.add("label"); //$NON-NLS-1$ |
| spanElements.add("q"); //$NON-NLS-1$ |
| spanElements.add("s"); //$NON-NLS-1$ |
| spanElements.add("samp"); //$NON-NLS-1$ |
| spanElements.add("small"); //$NON-NLS-1$ |
| spanElements.add("span"); //$NON-NLS-1$ |
| spanElements.add("strike"); //$NON-NLS-1$ |
| spanElements.add("strong"); //$NON-NLS-1$ |
| spanElements.add("sub"); //$NON-NLS-1$ |
| spanElements.add("sup"); //$NON-NLS-1$ |
| spanElements.add("tt"); //$NON-NLS-1$ |
| spanElements.add("u"); //$NON-NLS-1$ |
| spanElements.add("var"); //$NON-NLS-1$ |
| |
| blockElements.add("div"); //$NON-NLS-1$ |
| blockElements.add("dl"); //$NON-NLS-1$ |
| blockElements.add("form"); //$NON-NLS-1$ |
| blockElements.add("h1"); //$NON-NLS-1$ |
| blockElements.add("h2"); //$NON-NLS-1$ |
| blockElements.add("h3"); //$NON-NLS-1$ |
| blockElements.add("h4"); //$NON-NLS-1$ |
| blockElements.add("h5"); //$NON-NLS-1$ |
| blockElements.add("h6"); //$NON-NLS-1$ |
| blockElements.add("ol"); //$NON-NLS-1$ |
| blockElements.add("p"); //$NON-NLS-1$ |
| blockElements.add("pre"); //$NON-NLS-1$ |
| blockElements.add("table"); //$NON-NLS-1$ |
| blockElements.add("textarea"); //$NON-NLS-1$ |
| blockElements.add("td"); //$NON-NLS-1$ |
| blockElements.add("tr"); //$NON-NLS-1$ |
| blockElements.add("ul"); //$NON-NLS-1$ |
| blockElements.add("tbody"); //$NON-NLS-1$ |
| blockElements.add("thead"); //$NON-NLS-1$ |
| blockElements.add("tfoot"); //$NON-NLS-1$ |
| blockElements.add("li"); //$NON-NLS-1$ |
| blockElements.add("dd"); //$NON-NLS-1$ |
| blockElements.add("dt"); //$NON-NLS-1$ |
| |
| whitespaceCollapsingElements.add("br"); //$NON-NLS-1$ |
| whitespaceCollapsingElements.add("hr"); //$NON-NLS-1$ |
| } |
| |
| private static class ElementState implements ElementInfo { |
| String elementName; |
| |
| int childCount = 0; |
| |
| int textChildCount = 0; |
| |
| @SuppressWarnings("unused") |
| final int originalOffset; |
| |
| int offset; |
| |
| boolean skipWhitespace = true; |
| |
| @SuppressWarnings("unused") |
| boolean spanElement; |
| |
| boolean blockElement; |
| |
| boolean noWhitespaceTextContainer; |
| |
| boolean collapsesAdjacentWhitespace; |
| |
| final FontState fontState; |
| |
| int orderedListIndex = 0; |
| |
| int indentLevel = 0; |
| |
| int bulletLevel = 0; |
| |
| List<Annotation> annotations; |
| |
| char[] prefix; |
| |
| List<Annotation> prefixAnnotations; |
| |
| /** |
| * The last child that was just processed for this element |
| */ |
| ElementState lastChild; |
| |
| final ElementState parent; |
| |
| private String id; |
| |
| private String[] cssClasses; |
| |
| public int textOffset; |
| |
| public ElementState(ElementState parent, String elementName, ElementState elementState, int offset, |
| Attributes atts) { |
| this.parent = parent; |
| this.elementName = elementName; |
| this.fontState = new FontState(elementState.fontState); |
| this.offset = offset; |
| this.originalOffset = offset; |
| this.skipWhitespace = elementState.skipWhitespace; |
| this.indentLevel = elementState.indentLevel; |
| this.bulletLevel = elementState.bulletLevel; |
| initState(); |
| String cssClass = null; |
| for (int x = 0; x < atts.getLength(); ++x) { |
| String localName = atts.getLocalName(x); |
| if ("id".equals(localName)) { //$NON-NLS-1$ |
| this.id = atts.getValue(x); |
| } else if ("class".equals(localName)) { //$NON-NLS-1$ |
| cssClass = atts.getValue(x); |
| } |
| if (id != null && cssClass != null) { |
| break; |
| } |
| } |
| if (cssClass != null) { |
| cssClasses = cssClass.split("\\s+"); //$NON-NLS-1$ |
| if (cssClasses.length > 1) { |
| Arrays.sort(cssClasses); |
| } |
| } |
| } |
| |
| public ElementState(ElementState parent, String elementName, FontState fontState, int offset) { |
| this.parent = parent; |
| this.elementName = elementName; |
| this.fontState = new FontState(fontState); |
| this.offset = offset; |
| this.originalOffset = offset; |
| initState(); |
| } |
| |
| private void initState() { |
| String elementName = this.elementName.toLowerCase(); |
| spanElement = spanElements.contains(elementName); |
| blockElement = blockElements.contains(elementName); |
| collapsesAdjacentWhitespace = whitespaceCollapsingElements.contains(elementName); |
| noWhitespaceTextContainer = "body".equals(elementName); //$NON-NLS-1$ |
| } |
| |
| public void addAnnotation(Annotation annotation) { |
| if (annotations == null) { |
| annotations = new ArrayList<>(2); |
| } |
| annotations.add(annotation); |
| } |
| |
| public void addPrefixAnnotation(Annotation annotation) { |
| if (prefixAnnotations == null) { |
| prefixAnnotations = new ArrayList<>(1); |
| } |
| prefixAnnotations.add(annotation); |
| } |
| |
| public String getLocalName() { |
| return elementName; |
| } |
| |
| public ElementInfo getParent() { |
| return parent; |
| } |
| |
| public boolean hasCssClass(String cssClass) { |
| return cssClasses != null && Arrays.binarySearch(cssClasses, cssClass) >= 0; |
| } |
| |
| public boolean hasId(String id) { |
| return id != null && id.equals(this.id); |
| } |
| } |
| |
| private IAnnotationModel annotationModel; |
| |
| private TextPresentation presentation; |
| |
| private String text; |
| |
| private Font defaultFont; |
| |
| private Font defaultMonospaceFont; |
| |
| private Color defaultForeground; |
| |
| private Color defaultBackground; |
| |
| private char[] bulletChars = new char[] { '\u2022', // solid round bullet, see http://www.fileformat.info/info/unicode/char/2022/index.htm |
| // '\u26AA', // empty round bullet, see http://www.fileformat.info/info/unicode/char/26AA/index.htm |
| // '\u25A0', // square bullet, see http://www.fileformat.info/info/unicode/char/25A0/index.htm |
| }; |
| |
| private CssStyleManager cssStyleManager; |
| |
| private boolean enableImages = false; |
| |
| private ImageCache imageCache = new ImageCache(); |
| |
| private Stylesheet stylesheet = getDefaultStylesheet(); |
| |
| private final CssParser cssParser = new CssParser(); |
| |
| public HtmlTextPresentationParser() { |
| } |
| |
| private static Stylesheet getDefaultStylesheet() { |
| synchronized (HtmlTextPresentationParser.class) { |
| if (defaultStylesheet == null) { |
| try { |
| Reader reader = getDefaultStylesheetContent(); |
| try { |
| defaultStylesheet = new CssParser().parse(reader); |
| } finally { |
| reader.close(); |
| } |
| } catch (IOException e) { |
| throw new IllegalStateException(e); |
| } |
| } |
| return defaultStylesheet; |
| } |
| } |
| |
| public static Reader getDefaultStylesheetContent() throws IOException { |
| return new InputStreamReader(HtmlTextPresentationParser.class.getResourceAsStream("default.css"), //$NON-NLS-1$ |
| StandardCharsets.UTF_8); |
| } |
| |
| public TextPresentation getPresentation() { |
| return presentation; |
| } |
| |
| public void setPresentation(TextPresentation presentation) { |
| this.presentation = presentation; |
| if (presentation != null && presentation.getDefaultStyleRange() != null) { |
| if (presentation.getDefaultStyleRange().font != null) { |
| this.defaultFont = presentation.getDefaultStyleRange().font; |
| } |
| if (presentation.getDefaultStyleRange().foreground != null) { |
| this.defaultForeground = presentation.getDefaultStyleRange().foreground; |
| } |
| if (presentation.getDefaultStyleRange().foreground != null) { |
| this.defaultForeground = presentation.getDefaultStyleRange().background; |
| } |
| } |
| } |
| |
| public String getText() { |
| return text; |
| } |
| |
| public Font getDefaultFont() { |
| return defaultFont; |
| } |
| |
| public void setDefaultFont(Font defaultFont) { |
| this.defaultFont = defaultFont; |
| } |
| |
| public Font getDefaultMonospaceFont() { |
| return defaultMonospaceFont; |
| } |
| |
| public void setDefaultMonospaceFont(Font defaultMonospaceFont) { |
| this.defaultMonospaceFont = defaultMonospaceFont; |
| } |
| |
| public Color getDefaultForeground() { |
| return defaultForeground; |
| } |
| |
| public void setDefaultForeground(Color defaultForeground) { |
| this.defaultForeground = defaultForeground; |
| } |
| |
| public Color getDefaultBackground() { |
| return defaultBackground; |
| } |
| |
| public void setDefaultBackground(Color defaultBackground) { |
| this.defaultBackground = defaultBackground; |
| } |
| |
| public Stylesheet getStylesheet() { |
| return stylesheet; |
| } |
| |
| public void setStylesheet(Stylesheet stylesheet) { |
| this.stylesheet = stylesheet; |
| } |
| |
| /** |
| * Get the annotation model in which annotations are collected |
| * |
| * @return the annotation model, or null if there is none. |
| */ |
| public IAnnotationModel getAnnotationModel() { |
| return annotationModel; |
| } |
| |
| /** |
| * Set the annotation model if the parsing process should collect annotations for things like anchors and hover |
| * info. |
| * |
| * @param annotationModel |
| * the annotation model, or null if annotations should not be collected. |
| */ |
| public void setAnnotationModel(IAnnotationModel annotationModel) { |
| this.annotationModel = annotationModel; |
| } |
| |
| /** |
| * The maximum width in pixels. Only used when {@link #setGC(GC) a GC is provided}. |
| */ |
| public void setMaxWidth(int maxWidth) { |
| this.maxWidth = maxWidth; |
| } |
| |
| /** |
| * The GC, to be used in conjunction with {@link #setMaxWidth(int) the maximum width} |
| */ |
| public void setGC(GC gc) { |
| this.gc = gc; |
| } |
| |
| public void parse(String xhtmlContent) throws SAXException, IOException { |
| parse(new InputSource(new StringReader(xhtmlContent))); |
| } |
| |
| public void parse(InputSource xhtmlInput) throws SAXException, IOException { |
| if (presentation == null) { |
| throw new IllegalStateException(Messages.HtmlTextPresentationParser_presentationRequired); |
| } |
| if (defaultFont == null) { |
| throw new IllegalStateException(Messages.HtmlTextPresentationParser_defaultFontRequired); |
| } |
| |
| cssStyleManager = new CssStyleManager(defaultFont, defaultMonospaceFont); |
| |
| SAXParserFactory factory = SAXParserFactory.newInstance(); |
| |
| factory.setNamespaceAware(true); |
| factory.setValidating(false); |
| SAXParser saxParser; |
| try { |
| saxParser = factory.newSAXParser(); |
| } catch (ParserConfigurationException e) { |
| throw new IllegalStateException(e); |
| } |
| XMLReader parser = saxParser.getXMLReader(); |
| parser.setEntityResolver(IgnoreDtdEntityResolver.getInstance()); |
| parser.setContentHandler(new HtmlContentHandler()); |
| parser.parse(xhtmlInput); |
| } |
| |
| private class HtmlContentHandler implements ContentHandler { |
| |
| private final Stack<ElementState> state = new Stack<>(); |
| |
| private int lastNewlineOffset = 0; |
| |
| private final StringBuilder out = new StringBuilder(2048); |
| |
| private final List<StyleRange> styleRanges = new ArrayList<>(); |
| |
| private final Map<Annotation, Position> annotationToPosition = new IdentityHashMap<>(); |
| |
| private final StringBuilder elementText = new StringBuilder(); |
| |
| public void characters(char[] ch, int start, int length) throws SAXException { |
| if (!state.isEmpty()) { |
| ElementState elementState = state.peek(); |
| if (elementState.noWhitespaceTextContainer |
| || (elementState.blockElement && elementState.skipWhitespace && elementState.textChildCount == 0 |
| && elementState.childCount == 0) |
| || (elementState.lastChild != null && elementState.lastChild.collapsesAdjacentWhitespace)) { |
| // trim left here, since we must properly eliminate whitespace in ordered lists where we've already |
| // prepended a number to the list item text |
| int skip = 0; |
| while (skip < length && Character.isWhitespace(ch[start + skip])) { |
| ++skip; |
| } |
| start += skip; |
| length -= skip; |
| } |
| |
| // bug 274882 receiving characters makes the last element child irrelevant |
| elementState.lastChild = null; |
| |
| if (length != 0) { |
| ++elementState.textChildCount; |
| append(elementState, ch, start, length); |
| } |
| } |
| } |
| |
| private void append(ElementState elementState, char[] ch, int start, int length) { |
| if (elementState.skipWhitespace) { |
| // collapse adjacent whitespace, and replace newlines with a space character |
| int previousWhitespaceIndex = Integer.MIN_VALUE; |
| for (int x = 0; x < length; ++x) { |
| int index = start + x; |
| char c = ch[index]; |
| if (Character.isWhitespace(c)) { |
| if (previousWhitespaceIndex == index - 1) { |
| previousWhitespaceIndex = index; |
| continue; |
| } |
| previousWhitespaceIndex = index; |
| elementText.append(c == '\t' ? c : ' '); |
| } else { |
| elementText.append(c); |
| } |
| } |
| } else { |
| elementText.append(ch, start, length); |
| } |
| } |
| |
| public void emitText(ElementState elementState, boolean elementClosing) { |
| elementState.textOffset = elementState.offset; |
| if (state.isEmpty() || elementText.length() == 0) { |
| return; |
| } |
| String text = elementText.toString(); |
| if (elementState.skipWhitespace) { |
| if (elementClosing) { |
| if (elementState.childCount == 0) { |
| if (elementState.blockElement) { |
| text = text.trim(); |
| } |
| } else { |
| if (elementState.blockElement) { |
| text = trimRight(text); |
| } |
| } |
| } else { |
| // careful: this can result in losing significant whitespace |
| String originalText = text; |
| if (elementState.blockElement && elementState.childCount == 0) { |
| text = trimLeft(text); |
| if (text.length() == 0 && originalText.length() > 0) { |
| text = originalText.substring(0, 1); |
| } |
| } |
| } |
| } |
| elementText.delete(0, elementText.length()); |
| if (text.length() > 0) { |
| emitChars(elementState, text.toCharArray()); |
| } |
| } |
| |
| private void emitChar(char c) { |
| out.append(c); |
| lastNewlineOffset = getOffset(); |
| } |
| |
| private void emitChars(ElementState elementState, char[] chars) { |
| int indentLevel = elementState.indentLevel; |
| |
| boolean enforceMaxWidth = maxWidth > 0 && gc != null; |
| |
| // find the offset of the last natural break |
| int lastBreakPosition = lastNewlineOffset + 1; |
| if (enforceMaxWidth) { |
| for (int x = out.length() - 1; x >= 0; --x) { |
| char ch = out.charAt(x); |
| if (x == (lastNewlineOffset + 1)) { |
| break; |
| } |
| |
| if (ch == '-') { |
| lastBreakPosition = x + 1; |
| break; |
| } else if (Character.isWhitespace(ch)) { |
| lastBreakPosition = x; |
| break; |
| } |
| |
| } |
| } |
| |
| for (int x = 0; x < chars.length; ++x) { |
| char c = chars[x]; |
| if (lastNewlineOffset == getOffset() && (c != '\n' && c != '\r') && indentLevel > 0) { |
| for (int y = 0; y < indentLevel; ++y) { |
| out.append('\t'); |
| } |
| } |
| if (x == 0) { |
| char[] prefix = computePrefix(elementState); |
| if (prefix != null) { |
| int offset = out.length(); |
| for (char element : prefix) { |
| out.append(element); |
| } |
| List<Annotation> prefixAnnotations = computePrefixAnnotations(elementState); |
| if (prefixAnnotations != null && annotationModel != null) { |
| for (Annotation annotation : prefixAnnotations) { |
| annotationModel.addAnnotation(annotation, new Position(offset, 1)); |
| } |
| } |
| } |
| elementState.textOffset = getOffset(); |
| } |
| out.append(c); |
| |
| if (c == '-') { |
| lastBreakPosition = getOffset() + 1; |
| } else if (Character.isWhitespace(c)) { |
| lastBreakPosition = getOffset(); |
| } |
| |
| if (c == '\n') { |
| lastNewlineOffset = getOffset(); |
| lastBreakPosition = getOffset() + 1; |
| } else if (enforceMaxWidth) { |
| Point extent = gc.textExtent(out.substring(lastNewlineOffset + 1, out.length())); |
| final int rightMargin = 2; |
| if (extent.x >= (maxWidth - rightMargin)) { |
| if (lastBreakPosition <= getOffset()) { |
| out.insert(lastBreakPosition, '\n'); |
| lastNewlineOffset = lastBreakPosition; |
| lastBreakPosition = lastNewlineOffset + 1; |
| } else { |
| out.append('\n'); |
| lastNewlineOffset = getOffset(); |
| lastBreakPosition = getOffset() + 1; |
| } |
| } |
| } |
| } |
| } |
| |
| public void endElement(String uri, String localName, String name) throws SAXException { |
| ElementState elementState = state.peek(); |
| |
| emitText(elementState, true); |
| emitStyles(); |
| |
| if (elementState.annotations != null) { |
| for (Annotation annotation : elementState.annotations) { |
| annotationToPosition.put(annotation, |
| new Position(elementState.textOffset, getOffset() - elementState.textOffset)); |
| } |
| } |
| |
| char[] ch = elementToCharacters.get(localName.toLowerCase()); |
| if (ch != null) { |
| boolean skip = false; |
| if (state.size() > 1) { |
| if (elementState.elementName.equals("ul") || elementState.elementName.equals("ol") //$NON-NLS-1$ //$NON-NLS-2$ |
| || elementState.elementName.equals("dl")) { //$NON-NLS-1$ |
| ElementState parentState = state.get(state.size() - 2); |
| if (parentState.elementName.equals("li") || parentState.elementName.equals("dt") //$NON-NLS-1$ //$NON-NLS-2$ |
| || parentState.elementName.equals("dd")) { //$NON-NLS-1$ |
| skip = true; |
| } |
| } |
| } |
| if (!skip) { |
| emitPartial(elementState, ch); |
| } |
| } |
| |
| ElementState lastChild = state.pop(); |
| |
| if (localName.equals("hr")) { //$NON-NLS-1$ |
| emitChar('\n'); |
| } else if (localName.equals("img")) { //$NON-NLS-1$ |
| if (enableImages) { |
| for (Annotation annotation : elementState.annotations) { |
| if (annotation instanceof ImageAnnotation) { |
| ImageAnnotation imageAnnotation = (ImageAnnotation) annotation; |
| // ensure that the image is painted on a new line |
| if (out.length() > 0) { |
| char lastChar = out.charAt(out.length() - 1); |
| if (lastChar != '\n' && lastChar != '\r') { |
| emitChar('\n'); |
| Position position = annotationToPosition.get(imageAnnotation); |
| annotationToPosition.put(imageAnnotation, |
| new Position(position.getOffset() + 1, position.getLength())); |
| } |
| } |
| if (imageAnnotation.getImage() != null) { |
| // ensure that there are enough blank lines to display |
| // the image |
| int height = imageAnnotation.getImage().getBounds().height; |
| gc.setFont(defaultFont); |
| Point extent = gc.textExtent("\n"); //$NON-NLS-1$ |
| if (extent.y > 0) { |
| int numNewlines = (int) Math.ceil(((double) height) / ((double) extent.y)); |
| for (int x = 0; x < numNewlines && x < 1000; ++x) { |
| emitChar('\n'); |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| if (!state.isEmpty()) { |
| elementState = state.peek(); |
| elementState.offset = getOffset(); |
| elementState.lastChild = lastChild; |
| } |
| } |
| |
| private void emitPartial(ElementState elementState, char[] ch) { |
| int matchShift = -1; |
| for (int shift = 0; shift < ch.length; ++shift) { |
| if (endsWith(out, ch, shift, ch.length - shift)) { |
| matchShift = shift; |
| break; |
| } |
| } |
| if (matchShift > 0) { |
| char[] c2 = new char[ch.length - matchShift]; |
| System.arraycopy(ch, matchShift, c2, 0, c2.length); |
| emitChars(elementState, c2); |
| } else if (matchShift == -1) { |
| emitChars(elementState, ch); |
| } |
| } |
| |
| private boolean endsWith(StringBuilder out, char[] ch, int offset, int length) { |
| if (out.length() >= length) { |
| for (int x = 0; x < length; ++x) { |
| if (out.charAt((out.length() - length) + x) != ch[x + offset]) { |
| return false; |
| } |
| } |
| return true; |
| } |
| return false; |
| } |
| |
| public void startElement(String uri, String localName, String name, Attributes atts) throws SAXException { |
| final ElementState parentElementState = state.peek(); |
| |
| emitText(parentElementState, false); |
| |
| emitStyles(); |
| |
| ++parentElementState.childCount; |
| |
| final ElementState elementState = state |
| .push(new ElementState(parentElementState, localName, state.peek(), getOffset(), atts)); |
| if ("pre".equals(localName)) { //$NON-NLS-1$ |
| elementState.skipWhitespace = false; |
| } else if ("ul".equals(localName) || "ol".equals(localName)) { //$NON-NLS-1$ //$NON-NLS-2$ |
| ++elementState.indentLevel; |
| ++elementState.bulletLevel; |
| } else if ("blockquote".equals(localName) || "dd".equals(localName)) { //$NON-NLS-1$ //$NON-NLS-2$ |
| ++elementState.indentLevel; |
| } |
| // process stylesheet |
| stylesheet.applyTo(elementState, new Stylesheet.Receiver() { |
| public void apply(CssRule rule) { |
| cssStyleManager.processCssStyles(elementState.fontState, parentElementState.fontState, rule); |
| } |
| }); |
| |
| int numAtts = atts.getLength(); |
| for (int x = 0; x < numAtts; ++x) { |
| String attName = atts.getLocalName(x); |
| if ("style".equals(attName)) { //$NON-NLS-1$ |
| String styleValue = atts.getValue(x); |
| if (styleValue != null) { |
| Iterator<CssRule> ruleIterator = cssParser.createRuleIterator(styleValue); |
| while (ruleIterator.hasNext()) { |
| cssStyleManager.processCssStyles(elementState.fontState, parentElementState.fontState, |
| ruleIterator.next()); |
| } |
| } |
| } else if ("id".equals(attName)) { //$NON-NLS-1$ |
| elementState.addAnnotation(new IdAnnotation(atts.getValue(x))); |
| } else if ("href".equals(attName)) { //$NON-NLS-1$ |
| elementState.addAnnotation(new AnchorHrefAnnotation(atts.getValue(x))); |
| } else if ("href".equals(attName)) { //$NON-NLS-1$ |
| elementState.addAnnotation(new TitleAnnotation(atts.getValue(x), localName)); |
| } else if ("name".equals(attName)) { //$NON-NLS-1$ |
| if ("a".equals(localName)) { //$NON-NLS-1$ |
| elementState.addAnnotation(new AnchorNameAnnotation(atts.getValue(x))); |
| } |
| } else if ("class".equals(attName)) { //$NON-NLS-1$ |
| elementState.addAnnotation(new ClassAnnotation(atts.getValue(x))); |
| } else if ("title".equals(attName)) { //$NON-NLS-1$ |
| elementState.addAnnotation(new TitleAnnotation(atts.getValue(x), localName)); |
| } else if ("start".equals(attName)) { //$NON-NLS-1$ |
| if ("ol".equals(localName)) { //$NON-NLS-1$ |
| // bug 265015 honor the start attribute |
| try { |
| elementState.orderedListIndex = Integer.parseInt(atts.getValue(x), 10) - 1; |
| } catch (NumberFormatException e) { |
| // ignore |
| } |
| } |
| } |
| } |
| if ("li".equals(localName)) { //$NON-NLS-1$ |
| ElementState parentState = state.size() > 1 ? state.get(state.size() - 2) : null; |
| boolean numeric = parentState == null ? false : parentState.elementName.equals("ol"); //$NON-NLS-1$ |
| |
| int index = parentState == null ? 1 : ++parentState.orderedListIndex; |
| if (lastNewlineOffset != getOffset()) { |
| emitChars(state.peek(), "\n".toCharArray()); //$NON-NLS-1$ |
| } |
| if (numeric) { |
| elementState.prefix = (Integer.toString(index) + ". ").toCharArray(); //$NON-NLS-1$ |
| } else { |
| elementState.prefix = new char[] { calculateBulletChar(elementState.indentLevel), ' ', ' ' }; |
| elementState.addPrefixAnnotation(new BulletAnnotation(elementState.bulletLevel)); |
| } |
| } else if ("p".equals(localName)) { //$NON-NLS-1$ |
| // account for the case of a paragraph following an unescaped block (eg: Textile with first char being a space). |
| if (out.length() > 0) { |
| char lastChar = out.charAt(out.length() - 1); |
| if (lastChar != '\n') { |
| emitChars(state.peek(), "\n\n".toCharArray()); //$NON-NLS-1$ |
| } |
| } |
| } else if ("hr".equals(localName)) { //$NON-NLS-1$ |
| elementState.addAnnotation(new HorizontalRuleAnnotation()); |
| } else if ("img".equals(localName) && enableImages) { //$NON-NLS-1$ |
| String url = atts.getValue("src"); //$NON-NLS-1$ |
| if (url != null && url.trim().length() > 0) { |
| ImageAnnotation imageAnnotation = new ImageAnnotation(url.trim(), imageCache.getMissingImage()); |
| elementState.addAnnotation(imageAnnotation); |
| |
| // bug 257868: hook up hyperlinks to images |
| ElementState imageAnnotationAncestorState = elementState; |
| while (imageAnnotationAncestorState != null) { |
| |
| if (imageAnnotationAncestorState.annotations != null) { |
| for (Annotation annotation : imageAnnotationAncestorState.annotations) { |
| if (annotation instanceof AnchorHrefAnnotation) { |
| imageAnnotation.setAnchorHrefAnnotation((AnchorHrefAnnotation) annotation); |
| break; |
| } |
| } |
| } |
| if (imageAnnotation.getHyperlnkAnnotation() != null) { |
| break; |
| } |
| |
| imageAnnotationAncestorState = imageAnnotationAncestorState.parent; |
| } |
| } |
| } |
| } |
| |
| private char calculateBulletChar(int indentLevel) { |
| return bulletChars[Math.min(indentLevel - 1, bulletChars.length - 1)]; |
| } |
| |
| public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException { |
| if (!state.isEmpty()) { |
| ElementState elementState = state.peek(); |
| if (!elementState.skipWhitespace) { |
| ++elementState.textChildCount; |
| characters(ch, start, length); |
| } |
| } |
| } |
| |
| public void endDocument() throws SAXException { |
| // ORDER DEPENDENCY: do this first |
| if (annotationModel != null) { |
| for (Map.Entry<Annotation, Position> a : annotationToPosition.entrySet()) { |
| annotationModel.addAnnotation(a.getKey(), a.getValue()); |
| } |
| } |
| |
| // bug# 236787 trim trailing whitespace and adjust style ranges. |
| trimTrailingWhitespace(); |
| |
| text = out.toString(); |
| presentation.replaceStyleRanges(styleRanges.toArray(new StyleRange[styleRanges.size()])); |
| if (annotationModel != null) { |
| String idPrefix = IdAnnotation.class.getPackage().getName(); |
| Iterator<?> annotationIterator = annotationModel.getAnnotationIterator(); |
| while (annotationIterator.hasNext()) { |
| Annotation annotation = (Annotation) annotationIterator.next(); |
| if (annotation.getType().startsWith(idPrefix) && !annotationToPosition.containsKey(annotation)) { |
| annotationIterator.remove(); |
| } |
| } |
| } |
| |
| state.clear(); |
| } |
| |
| /** |
| * trim trailing whitespace from the output buffer and adjust style ranges and annotations |
| */ |
| private void trimTrailingWhitespace() { |
| int length = out.length(); |
| for (int x = length - 1; x >= 0; --x) { |
| if (Character.isWhitespace(out.charAt(x))) { |
| if (Util.annotationsIncludeOffset(annotationModel, x)) { |
| return; |
| } |
| length = x; |
| } else { |
| break; |
| } |
| } |
| if (length != out.length()) { |
| out.delete(length, out.length()); |
| Iterator<StyleRange> styleIt = styleRanges.iterator(); |
| while (styleIt.hasNext()) { |
| StyleRange styleRange = styleIt.next(); |
| if (styleRange.start >= length) { |
| styleIt.remove(); |
| } else { |
| int styleEnd = styleRange.start + styleRange.length; |
| if (styleEnd > length) { |
| styleRange.length -= styleEnd - length; |
| } |
| } |
| } |
| } |
| } |
| |
| public void startDocument() throws SAXException { |
| ElementState elementState = state.push(new ElementState(null, "<document>", new FontState(), getOffset())); //$NON-NLS-1$ |
| elementState.fontState.foreground = defaultForeground == null ? null : defaultForeground.getRGB(); |
| elementState.fontState.background = defaultBackground == null ? null : defaultBackground.getRGB(); |
| } |
| |
| public void processingInstruction(String target, String data) throws SAXException { |
| } |
| |
| public void skippedEntity(String name) throws SAXException { |
| } |
| |
| public void setDocumentLocator(Locator locator) { |
| } |
| |
| public void startPrefixMapping(String prefix, String uri) throws SAXException { |
| } |
| |
| public void endPrefixMapping(String prefix) throws SAXException { |
| } |
| |
| private void emitStyles() { |
| if (state.isEmpty()) { |
| return; |
| } |
| ElementState elementState = state.peek(); |
| int offset = elementState.offset; |
| if (offset >= getOffset()) { |
| // 0-length styles?? |
| return; |
| } |
| if (elementState.fontState.equals(state.get(0).fontState)) { |
| // no different than the default state |
| return; |
| } |
| int length = getOffset() - offset; |
| boolean underline = elementState.fontState.isUnderline(); |
| boolean strikethrough = elementState.fontState.isStrikethrough(); |
| boolean split = offset != elementState.textOffset && (underline || strikethrough); |
| if (split) { |
| length = elementState.textOffset - offset; |
| elementState.fontState.setStrikethrough(false); |
| elementState.fontState.setUnderline(false); |
| } |
| StyleRange styleRange = cssStyleManager.createStyleRange(elementState.fontState, offset, length); |
| styleRanges.add(styleRange); |
| if (split) { |
| offset = elementState.textOffset; |
| length = getOffset() - elementState.textOffset; |
| elementState.fontState.setStrikethrough(strikethrough); |
| elementState.fontState.setUnderline(underline); |
| |
| styleRange = cssStyleManager.createStyleRange(elementState.fontState, offset, length); |
| styleRanges.add(styleRange); |
| } |
| } |
| |
| private int getOffset() { |
| return out.length(); |
| } |
| |
| } |
| |
| private static Map<String, char[]> elementToCharacters = new HashMap<>(); |
| |
| static { |
| elementToCharacters.put("p", "\n\n".toCharArray()); //$NON-NLS-1$ //$NON-NLS-2$ |
| elementToCharacters.put("br", "\n".toCharArray()); //$NON-NLS-1$ //$NON-NLS-2$ |
| elementToCharacters.put("tr", "\n".toCharArray()); //$NON-NLS-1$ //$NON-NLS-2$ |
| elementToCharacters.put("table", "\n\n".toCharArray()); //$NON-NLS-1$ //$NON-NLS-2$ |
| elementToCharacters.put("ol", "\n\n".toCharArray()); //$NON-NLS-1$ //$NON-NLS-2$ |
| elementToCharacters.put("ul", "\n\n".toCharArray()); //$NON-NLS-1$ //$NON-NLS-2$ |
| elementToCharacters.put("dl", "\n\n".toCharArray()); //$NON-NLS-1$ //$NON-NLS-2$ |
| elementToCharacters.put("h1", "\n\n".toCharArray()); //$NON-NLS-1$ //$NON-NLS-2$ |
| elementToCharacters.put("h2", "\n\n".toCharArray()); //$NON-NLS-1$ //$NON-NLS-2$ |
| elementToCharacters.put("h3", "\n\n".toCharArray()); //$NON-NLS-1$ //$NON-NLS-2$ |
| elementToCharacters.put("h4", "\n\n".toCharArray()); //$NON-NLS-1$ //$NON-NLS-2$ |
| elementToCharacters.put("h5", "\n\n".toCharArray()); //$NON-NLS-1$ //$NON-NLS-2$ |
| elementToCharacters.put("h6", "\n\n".toCharArray()); //$NON-NLS-1$ //$NON-NLS-2$ |
| elementToCharacters.put("blockquote", "\n\n".toCharArray()); //$NON-NLS-1$ //$NON-NLS-2$ |
| elementToCharacters.put("pre", "\n\n".toCharArray()); //$NON-NLS-1$ //$NON-NLS-2$ |
| elementToCharacters.put("th", " \t".toCharArray()); //$NON-NLS-1$ //$NON-NLS-2$ |
| elementToCharacters.put("td", " \t".toCharArray()); //$NON-NLS-1$ //$NON-NLS-2$ |
| elementToCharacters.put("dt", "\n".toCharArray()); //$NON-NLS-1$ //$NON-NLS-2$ |
| elementToCharacters.put("dd", "\n".toCharArray()); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| |
| private GC gc; |
| |
| private int maxWidth; |
| |
| private static String trimLeft(String text) { |
| final int len = text.length(); |
| int st = 0; |
| |
| while ((st < len) && (text.charAt(st) <= ' ')) { |
| st++; |
| } |
| return st > 0 ? text.substring(st, len) : text; |
| } |
| |
| public List<Annotation> computePrefixAnnotations(ElementState elementState) { |
| while (elementState != null) { |
| if (elementState.prefixAnnotations != null) { |
| List<Annotation> prefixAnnotations = elementState.prefixAnnotations; |
| elementState.prefixAnnotations = null; |
| return prefixAnnotations; |
| } |
| elementState = elementState.parent; |
| } |
| return null; |
| } |
| |
| public char[] computePrefix(ElementState elementState) { |
| while (elementState != null) { |
| if (elementState.prefix != null) { |
| char[] prefix = elementState.prefix; |
| elementState.prefix = null; |
| return prefix; |
| } |
| elementState = elementState.parent; |
| } |
| return null; |
| } |
| |
| private static String trimRight(String text) { |
| int len = text.length(); |
| |
| while (0 < len && (text.charAt(len - 1) <= ' ')) { |
| len--; |
| } |
| return len < text.length() ? text.substring(0, len) : text; |
| } |
| |
| /** |
| * get the bullet characters that are to be used when presenting bulleted lists. For an indent level of one, the |
| * first character is used, for indent level 2 the second character is used, etc unless the indent level exceeds the |
| * number of characters provided in which case the last character is used. Note that not all characters are |
| * available with all fonts. |
| */ |
| public char[] getBulletChars() { |
| return bulletChars; |
| } |
| |
| /** |
| * set the bullet characters that are to be used when presenting bulleted lists. For an indent level of one, the |
| * first character is used, for indent level 2 the second character is used, etc unless the indent level exceeds the |
| * number of characters provided in which case the last character is used. Note that not all characters are |
| * available with all fonts. |
| */ |
| public void setBulletChars(char[] bulletChars) { |
| this.bulletChars = bulletChars; |
| } |
| |
| /** |
| * indicate if image display is enabled. The default is false. |
| */ |
| public boolean isEnableImages() { |
| return enableImages; |
| } |
| |
| /** |
| * indicate if image display is enabled. The default is false. |
| */ |
| public void setEnableImages(boolean enableImages) { |
| this.enableImages = enableImages; |
| } |
| |
| public ImageCache getImageCache() { |
| return imageCache; |
| } |
| |
| public void setImageCache(ImageCache imageCache) { |
| this.imageCache = imageCache; |
| } |
| |
| } |