| /******************************************************************************* |
| * 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 v1.0 |
| * which accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * David Green - initial API and implementation |
| * Torkild U. Resheim - Handle links when transforming, bug 325006 |
| * Jeremie Bresson - Bug 492302 |
| *******************************************************************************/ |
| package org.eclipse.mylyn.wikitext.parser.builder; |
| |
| import static com.google.common.base.Preconditions.checkArgument; |
| import static com.google.common.base.Preconditions.checkNotNull; |
| |
| import java.io.BufferedReader; |
| import java.io.File; |
| import java.io.FileNotFoundException; |
| import java.io.FileReader; |
| import java.io.IOException; |
| import java.io.Reader; |
| import java.io.Writer; |
| import java.text.MessageFormat; |
| import java.util.ArrayList; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Stack; |
| import java.util.regex.Pattern; |
| |
| import org.eclipse.mylyn.wikitext.parser.Attributes; |
| import org.eclipse.mylyn.wikitext.parser.ImageAttributes; |
| import org.eclipse.mylyn.wikitext.parser.ImageAttributes.Align; |
| import org.eclipse.mylyn.wikitext.parser.LinkAttributes; |
| import org.eclipse.mylyn.wikitext.parser.ListAttributes; |
| import org.eclipse.mylyn.wikitext.parser.QuoteAttributes; |
| import org.eclipse.mylyn.wikitext.parser.TableAttributes; |
| import org.eclipse.mylyn.wikitext.parser.TableCellAttributes; |
| import org.eclipse.mylyn.wikitext.parser.TableRowAttributes; |
| import org.eclipse.mylyn.wikitext.util.DefaultXmlStreamWriter; |
| import org.eclipse.mylyn.wikitext.util.FormattingXMLStreamWriter; |
| import org.eclipse.mylyn.wikitext.util.XmlStreamWriter; |
| |
| import com.google.common.collect.ImmutableMap; |
| |
| /** |
| * A builder that produces XHTML output. The nature of the output is affected by various settings on the builder. |
| * |
| * @author David Green |
| * @author Matthias Kempka extensibility improvements, see bug 259089 |
| * @author Torkild U. Resheim |
| * @since 3.0 |
| */ |
| public class HtmlDocumentBuilder extends AbstractXmlDocumentBuilder { |
| |
| private static final Pattern ABSOLUTE_URL_PATTERN = Pattern.compile("[a-zA-Z]{3,8}://?.*"); //$NON-NLS-1$ |
| |
| private static final Map<SpanType, String> defaultSpanTypeToElementName; |
| |
| static { |
| ImmutableMap.Builder<SpanType, String> spanTypeToElementNameBuilder = ImmutableMap.builder(); |
| spanTypeToElementNameBuilder.put(SpanType.LINK, "a"); //$NON-NLS-1$ |
| spanTypeToElementNameBuilder.put(SpanType.BOLD, "b"); //$NON-NLS-1$ |
| spanTypeToElementNameBuilder.put(SpanType.CITATION, "cite"); //$NON-NLS-1$ |
| spanTypeToElementNameBuilder.put(SpanType.ITALIC, "i"); //$NON-NLS-1$ |
| spanTypeToElementNameBuilder.put(SpanType.EMPHASIS, "em"); //$NON-NLS-1$ |
| spanTypeToElementNameBuilder.put(SpanType.STRONG, "strong"); //$NON-NLS-1$ |
| spanTypeToElementNameBuilder.put(SpanType.DELETED, "del"); //$NON-NLS-1$ |
| spanTypeToElementNameBuilder.put(SpanType.INSERTED, "ins"); //$NON-NLS-1$ |
| spanTypeToElementNameBuilder.put(SpanType.QUOTE, "q"); //$NON-NLS-1$ |
| spanTypeToElementNameBuilder.put(SpanType.UNDERLINED, "u"); //$NON-NLS-1$ |
| spanTypeToElementNameBuilder.put(SpanType.SUPERSCRIPT, "sup"); //$NON-NLS-1$ |
| spanTypeToElementNameBuilder.put(SpanType.SUBSCRIPT, "sub"); //$NON-NLS-1$ |
| spanTypeToElementNameBuilder.put(SpanType.SPAN, "span"); //$NON-NLS-1$ |
| spanTypeToElementNameBuilder.put(SpanType.CODE, "code"); //$NON-NLS-1$ |
| spanTypeToElementNameBuilder.put(SpanType.MONOSPACE, "tt"); //$NON-NLS-1$ |
| spanTypeToElementNameBuilder.put(SpanType.MARK, "mark"); //$NON-NLS-1$ |
| defaultSpanTypeToElementName = spanTypeToElementNameBuilder.build(); |
| } |
| |
| private static final Map<BlockType, ElementInfo> blockTypeToElementInfo; |
| |
| static { |
| ImmutableMap.Builder<BlockType, ElementInfo> blockTypeToElementInfoBuilder = ImmutableMap.builder(); |
| blockTypeToElementInfoBuilder.put(BlockType.BULLETED_LIST, new ElementInfo("ul")); //$NON-NLS-1$ |
| blockTypeToElementInfoBuilder.put(BlockType.CODE, new ElementInfo("pre", null, null, new ElementInfo("code"))); //$NON-NLS-1$ //$NON-NLS-2$ |
| blockTypeToElementInfoBuilder.put(BlockType.DIV, new ElementInfo("div")); //$NON-NLS-1$ |
| blockTypeToElementInfoBuilder.put(BlockType.FOOTNOTE, new ElementInfo("footnote")); //$NON-NLS-1$ |
| blockTypeToElementInfoBuilder.put(BlockType.LIST_ITEM, new ElementInfo("li")); //$NON-NLS-1$ |
| blockTypeToElementInfoBuilder.put(BlockType.NUMERIC_LIST, new ElementInfo("ol")); //$NON-NLS-1$ |
| blockTypeToElementInfoBuilder.put(BlockType.DEFINITION_LIST, new ElementInfo("dl")); //$NON-NLS-1$ |
| blockTypeToElementInfoBuilder.put(BlockType.DEFINITION_TERM, new ElementInfo("dt")); //$NON-NLS-1$ |
| blockTypeToElementInfoBuilder.put(BlockType.DEFINITION_ITEM, new ElementInfo("dd")); //$NON-NLS-1$ |
| blockTypeToElementInfoBuilder.put(BlockType.PARAGRAPH, new ElementInfo("p")); //$NON-NLS-1$ |
| blockTypeToElementInfoBuilder.put(BlockType.PREFORMATTED, new ElementInfo("pre")); //$NON-NLS-1$ |
| blockTypeToElementInfoBuilder.put(BlockType.QUOTE, new ElementInfo("blockquote")); //$NON-NLS-1$ |
| blockTypeToElementInfoBuilder.put(BlockType.TABLE, new ElementInfo("table")); //$NON-NLS-1$ |
| blockTypeToElementInfoBuilder.put(BlockType.TABLE_CELL_HEADER, new ElementInfo("th")); //$NON-NLS-1$ |
| blockTypeToElementInfoBuilder.put(BlockType.TABLE_CELL_NORMAL, new ElementInfo("td")); //$NON-NLS-1$ |
| blockTypeToElementInfoBuilder.put(BlockType.TABLE_ROW, new ElementInfo("tr")); //$NON-NLS-1$ |
| blockTypeToElementInfoBuilder.put(BlockType.TIP, new ElementInfo("div", "tip", //$NON-NLS-1$ //$NON-NLS-2$ |
| "border: 1px solid #090;background-color: #dfd;margin: 20px;padding: 0px 6px 0px 6px;")); //$NON-NLS-1$ |
| blockTypeToElementInfoBuilder.put(BlockType.WARNING, new ElementInfo("div", "warning", //$NON-NLS-1$ //$NON-NLS-2$ |
| "border: 1px solid #c00;background-color: #fcc;margin: 20px;padding: 0px 6px 0px 6px;")); //$NON-NLS-1$ |
| blockTypeToElementInfoBuilder.put(BlockType.INFORMATION, new ElementInfo("div", "info", //$NON-NLS-1$ //$NON-NLS-2$ |
| "border: 1px solid #3c78b5;background-color: #D8E4F1;margin: 20px;padding: 0px 6px 0px 6px;")); //$NON-NLS-1$ |
| blockTypeToElementInfoBuilder.put(BlockType.NOTE, new ElementInfo("div", "note", //$NON-NLS-1$ //$NON-NLS-2$ |
| "border: 1px solid #F0C000;background-color: #FFFFCE;margin: 20px;padding: 0px 6px 0px 6px;")); //$NON-NLS-1$ |
| blockTypeToElementInfoBuilder.put(BlockType.PANEL, new ElementInfo("div", "panel", //$NON-NLS-1$ //$NON-NLS-2$ |
| "border: 1px solid #ccc;background-color: #FFFFCE;margin: 10px;padding: 0px 6px 0px 6px;")); //$NON-NLS-1$ |
| blockTypeToElementInfo = blockTypeToElementInfoBuilder.build(); |
| } |
| |
| private Map<SpanType, String> spanTypeToElementName = ImmutableMap.copyOf(defaultSpanTypeToElementName); |
| |
| private String htmlNsUri = "http://www.w3.org/1999/xhtml"; //$NON-NLS-1$ |
| |
| private String htmlDtd = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">"; //$NON-NLS-1$ |
| |
| private boolean xhtmlStrict = false; |
| |
| private boolean emitAsDocument = true; |
| |
| private boolean emitDtd = false; |
| |
| private String encoding = "utf-8"; //$NON-NLS-1$ |
| |
| private String title; |
| |
| private String defaultAbsoluteLinkTarget; |
| |
| private List<Stylesheet> stylesheets = null; |
| |
| private boolean useInlineStyles = true; |
| |
| private boolean suppressBuiltInStyles = false; |
| |
| private String linkRel; |
| |
| private String prependImagePrefix; |
| |
| private boolean filterEntityReferences = false; |
| |
| private String copyrightNotice; |
| |
| private String htmlFilenameFormat = null; |
| |
| private HtmlDocumentHandler documentHandler = new DefaultDocumentHandler(); |
| |
| private final Stack<ElementInfo> blockState = new Stack<ElementInfo>(); |
| |
| /** |
| * construct the HtmlDocumentBuilder. |
| * |
| * @param out |
| * the writer to which content is written |
| */ |
| public HtmlDocumentBuilder(Writer out) { |
| this(out, false); |
| } |
| |
| /** |
| * construct the HtmlDocumentBuilder. |
| * |
| * @param out |
| * the writer to which content is written |
| * @param formatting |
| * indicate if the output should be formatted |
| */ |
| public HtmlDocumentBuilder(Writer out, boolean formatting) { |
| super(formatting ? createFormattingXmlStreamWriter(out) : new DefaultXmlStreamWriter(out)); |
| } |
| |
| /** |
| * construct the HtmlDocumentBuilder. |
| * |
| * @param writer |
| * the writer to which content is written |
| */ |
| public HtmlDocumentBuilder(XmlStreamWriter writer) { |
| super(writer); |
| } |
| |
| /** |
| * Copy the configuration of this builder to the provided one. After calling this method the configuration of the |
| * other builder should be the same as this one, including stylesheets. Subclasses that have configurable settings |
| * should override this method to ensure that those settings are properly copied. |
| * |
| * @param other |
| * the builder to which settings are copied. |
| */ |
| public void copyConfiguration(HtmlDocumentBuilder other) { |
| other.setBase(getBase()); |
| other.setBaseInHead(isBaseInHead()); |
| other.setDefaultAbsoluteLinkTarget(getDefaultAbsoluteLinkTarget()); |
| other.setEmitAsDocument(isEmitAsDocument()); |
| other.setEmitDtd(isEmitDtd()); |
| other.setHtmlDtd(getHtmlDtd()); |
| other.setHtmlNsUri(getHtmlNsUri()); |
| other.setLinkRel(getLinkRel()); |
| other.setTitle(getTitle()); |
| other.setUseInlineStyles(isUseInlineStyles()); |
| other.setSuppressBuiltInStyles(isSuppressBuiltInStyles()); |
| other.setXhtmlStrict(xhtmlStrict); |
| other.setPrependImagePrefix(prependImagePrefix); |
| other.setCopyrightNotice(getCopyrightNotice()); |
| other.setHtmlFilenameFormat(htmlFilenameFormat); |
| other.spanTypeToElementName = spanTypeToElementName; |
| if (stylesheets != null) { |
| other.stylesheets = new ArrayList<Stylesheet>(); |
| other.stylesheets.addAll(stylesheets); |
| } |
| } |
| |
| protected static XmlStreamWriter createFormattingXmlStreamWriter(Writer out) { |
| return new FormattingXMLStreamWriter(new DefaultXmlStreamWriter(out)) { |
| @Override |
| protected boolean preserveWhitespace(String elementName) { |
| return elementName.equals("pre") || elementName.equals("code"); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| }; |
| } |
| |
| /** |
| * Provides an element name for the given {@code spanType} replacing the previous mapping. The new |
| * {@code elementName} is used when the corresponding {@link SpanType} is {@link #beginSpan(SpanType, Attributes) |
| * started}. |
| * |
| * @param spanType |
| * the span type |
| * @param elementName |
| * the element name to use in the generated HTML when emitting spans of the given type |
| */ |
| public void setElementNameOfSpanType(SpanType spanType, String elementName) { |
| checkNotNull(spanType, "Must provide spanType"); //$NON-NLS-1$ |
| checkNotNull(elementName, "Must provide elementName"); //$NON-NLS-1$ |
| |
| ImmutableMap.Builder<SpanType, String> builder = ImmutableMap.builder(); |
| for (Entry<SpanType, String> entry : spanTypeToElementName.entrySet()) { |
| if (!entry.getKey().equals(spanType)) { |
| builder.put(entry); |
| } |
| } |
| builder.put(spanType, elementName); |
| |
| spanTypeToElementName = builder.build(); |
| } |
| |
| /** |
| * The XML Namespace URI of the HTML elements, only used if {@link #isEmitAsDocument()}. The default value is " |
| * <code>http://www.w3.org/1999/xhtml</code>". |
| */ |
| public String getHtmlNsUri() { |
| return htmlNsUri; |
| } |
| |
| /** |
| * The XML Namespace URI of the HTML elements, only used if {@link #isEmitAsDocument()}. The default value is " |
| * <code>http://www.w3.org/1999/xhtml</code>". |
| */ |
| public void setHtmlNsUri(String htmlNsUri) { |
| this.htmlNsUri = htmlNsUri; |
| } |
| |
| /** |
| * The DTD to emit, if {@link #isEmitDtd()} and {@link #isEmitAsDocument()}. The default value is |
| * <code><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"></code> |
| */ |
| public String getHtmlDtd() { |
| return htmlDtd; |
| } |
| |
| /** |
| * The DTD to emit, if {@link #isEmitDtd()} and {@link #isEmitAsDocument()}. |
| * |
| * @see #getHtmlDtd() |
| */ |
| public void setHtmlDtd(String htmlDtd) { |
| this.htmlDtd = htmlDtd; |
| } |
| |
| /** |
| * Indicate if the resulting HTML should be emitted as a document. If false, the html and body tags are not included |
| * in the output. Default value is true. |
| */ |
| public boolean isEmitAsDocument() { |
| return emitAsDocument; |
| } |
| |
| /** |
| * Indicate if the resulting HTML should be emitted as a document. If false, the html and body tags are not included |
| * in the output. Default value is true. |
| */ |
| public void setEmitAsDocument(boolean emitAsDocument) { |
| this.emitAsDocument = emitAsDocument; |
| } |
| |
| /** |
| * Indicate if the resulting HTML should include a DTD. Ignored unless {@link #isEmitAsDocument()}. Default value is |
| * false. |
| */ |
| public boolean isEmitDtd() { |
| return emitDtd; |
| } |
| |
| /** |
| * Indicate if the resulting HTML should include a DTD. Ignored unless {@link #isEmitAsDocument()}. Default value is |
| * false. |
| */ |
| public void setEmitDtd(boolean emitDtd) { |
| this.emitDtd = emitDtd; |
| } |
| |
| /** |
| * Specify the character encoding for use in the HTML meta tag. For example, if the charset is specified as |
| * <code>"utf-8"</code>: <code><meta http-equiv="Content-Type" content="text/html; charset=utf-8"/></code> The |
| * default is <code>"utf-8"</code>. Ignored unless {@link #isEmitAsDocument()} |
| */ |
| public String getEncoding() { |
| return encoding; |
| } |
| |
| /** |
| * Specify the character encoding for use in the HTML meta tag. For example, if the charset is specified as |
| * <code>"utf-8"</code>: <code><meta http-equiv="Content-Type" content="text/html; charset=utf-8"/></code> The |
| * default is <code>"utf-8"</code>. |
| * |
| * @param encoding |
| * the character encoding to use, or null if the HTML meta tag should not be emitted Ignored unless |
| * {@link #isEmitAsDocument()} |
| */ |
| public void setEncoding(String encoding) { |
| this.encoding = encoding; |
| } |
| |
| /** |
| * Set the document title, which will be emitted into the <title> element. Ignored unless |
| * {@link #isEmitAsDocument()} |
| * |
| * @return the title or null if there is none |
| */ |
| public String getTitle() { |
| return title; |
| } |
| |
| /** |
| * Set the document title, which will be emitted into the <title> element. Ignored unless |
| * {@link #isEmitAsDocument()} |
| * |
| * @param title |
| * the title or null if there is none |
| */ |
| public void setTitle(String title) { |
| this.title = title; |
| } |
| |
| /** |
| * A default target attribute for links that have absolute (not relative) urls. By default this value is null. |
| * Setting this value will cause all HTML anchors to have their target attribute set if it's not explicitly |
| * specified in a {@link LinkAttributes}. |
| */ |
| public String getDefaultAbsoluteLinkTarget() { |
| return defaultAbsoluteLinkTarget; |
| } |
| |
| /** |
| * A default target attribute for links that have absolute (not relative) urls. By default this value is null. |
| * Setting this value will cause all HTML anchors to have their target attribute set if it's not explicitly |
| * specified in a {@link LinkAttributes}. |
| */ |
| public void setDefaultAbsoluteLinkTarget(String defaultAbsoluteLinkTarget) { |
| this.defaultAbsoluteLinkTarget = defaultAbsoluteLinkTarget; |
| } |
| |
| /** |
| * indicate if the builder should attempt to conform to strict XHTML rules. The default is false. |
| */ |
| public boolean isXhtmlStrict() { |
| return xhtmlStrict; |
| } |
| |
| /** |
| * indicate if the builder should attempt to conform to strict XHTML rules. The default is false. |
| */ |
| public void setXhtmlStrict(boolean xhtmlStrict) { |
| this.xhtmlStrict = xhtmlStrict; |
| } |
| |
| /** |
| * Add a CSS stylesheet to the output document as an URL, where the CSS stylesheet is referenced as an HTML link. |
| * Calling this method after {@link #beginDocument() starting the document} has no effect. Generates code similar to |
| * the following: <code> |
| * <link type="text/css" rel="stylesheet" href="url"/> |
| * </code> |
| * |
| * @param url |
| * the CSS url to use, which may be relative or absolute |
| * @return the stylesheet, whose attributes may be modified |
| * @see #addCssStylesheet(File) |
| * @deprecated use {@link #addCssStylesheet(Stylesheet)} instead |
| */ |
| @Deprecated |
| public void addCssStylesheet(String url) { |
| addCssStylesheet(new Stylesheet(url)); |
| } |
| |
| /** |
| * Add a CSS stylesheet to the output document, where the contents of the CSS stylesheet are embedded in the HTML. |
| * Calling this method after {@link #beginDocument() starting the document} has no effect. Generates code similar to |
| * the following: |
| * |
| * <pre> |
| * <code> |
| * <style type="text/css"> |
| * ... contents of the file ... |
| * </style> |
| * </code> |
| * </pre> |
| * |
| * @param file |
| * the CSS file whose contents must be available |
| * @return the stylesheet, whose attributes may be modified |
| * @see #addCssStylesheet(String) |
| * @deprecated use {@link #addCssStylesheet(Stylesheet)} instead |
| */ |
| @Deprecated |
| public void addCssStylesheet(File file) { |
| addCssStylesheet(new Stylesheet(file)); |
| } |
| |
| /** |
| * Add a CSS stylesheet to the output document. Calling this method after {@link #beginDocument() starting the |
| * document} has no effect. |
| */ |
| public void addCssStylesheet(Stylesheet stylesheet) { |
| if (stylesheet.file != null) { |
| checkFileReadable(stylesheet.file); |
| } |
| |
| if (stylesheets == null) { |
| stylesheets = new ArrayList<Stylesheet>(); |
| } |
| stylesheets.add(stylesheet); |
| } |
| |
| protected void checkFileReadable(File file) { |
| if (!file.exists()) { |
| throw new IllegalArgumentException(MessageFormat.format(Messages.getString("HtmlDocumentBuilder.3"), file)); //$NON-NLS-1$ |
| } |
| if (!file.isFile()) { |
| throw new IllegalArgumentException(MessageFormat.format(Messages.getString("HtmlDocumentBuilder.1"), file)); //$NON-NLS-1$ |
| } |
| if (!file.canRead()) { |
| throw new IllegalArgumentException(MessageFormat.format(Messages.getString("HtmlDocumentBuilder.2"), file)); //$NON-NLS-1$ |
| } |
| } |
| |
| /** |
| * Indicate if inline styles should be used when creating output such as text boxes. When disabled inline styles are |
| * suppressed and CSS classes are used instead, with the default styles emitted as a stylesheet in the document |
| * head. If disabled and {@link #isEmitAsDocument()} is false, this option has the same effect as |
| * {@link #isSuppressBuiltInStyles()}. The default is true. |
| * |
| * @see #isSuppressBuiltInStyles() |
| */ |
| public boolean isUseInlineStyles() { |
| return useInlineStyles; |
| } |
| |
| /** |
| * Indicate if inline styles should be used when creating output such as text boxes. When disabled inline styles are |
| * suppressed and CSS classes are used instead, with the default styles emitted as a stylesheet in the document |
| * head. If disabled and {@link #isEmitAsDocument()} is false, this option has the same effect as |
| * {@link #isSuppressBuiltInStyles()}. The default is true. |
| */ |
| public void setUseInlineStyles(boolean useInlineStyles) { |
| this.useInlineStyles = useInlineStyles; |
| } |
| |
| /** |
| * indicate if default built-in CSS styles should be suppressed. Built-in styles are styles that are emitted by this |
| * builder to create the desired visual effect when rendering certain types of elements, such as warnings or infos. |
| * the default is false. |
| * |
| * @see #isUseInlineStyles() |
| */ |
| public boolean isSuppressBuiltInStyles() { |
| return suppressBuiltInStyles; |
| } |
| |
| /** |
| * indicate if default built-in CSS styles should be suppressed. Built-in styles are styles that are emitted by this |
| * builder to create the desired visual effect when rendering certain types of elements, such as warnings or infos. |
| * the default is false. |
| */ |
| public void setSuppressBuiltInStyles(boolean suppressBuiltInStyles) { |
| this.suppressBuiltInStyles = suppressBuiltInStyles; |
| } |
| |
| /** |
| * The 'rel' value for HTML links. If specified the value is applied to all links generated by the builder. The |
| * default value is null. Setting this value to "nofollow" is recommended for rendering HTML in areas where users |
| * may add links, for example in a blog comment. See |
| * <a href="http://en.wikipedia.org/wiki/Nofollow">http://en.wikipedia.org/wiki/Nofollow</a> for more information. |
| * |
| * @return the rel or null if there is none. |
| * @see LinkAttributes#getRel() |
| */ |
| public String getLinkRel() { |
| return linkRel; |
| } |
| |
| /** |
| * The 'rel' value for HTML links. If specified the value is applied to all links generated by the builder. The |
| * default value is null. Setting this value to "nofollow" is recommended for rendering HTML in areas where users |
| * may add links, for example in a blog comment. See |
| * <a href="http://en.wikipedia.org/wiki/Nofollow">http://en.wikipedia.org/wiki/Nofollow</a> for more information. |
| * |
| * @param linkRel |
| * the rel or null if there is none. |
| * @see LinkAttributes#getRel() |
| */ |
| public void setLinkRel(String linkRel) { |
| this.linkRel = linkRel; |
| } |
| |
| /** |
| * Provides an {@link HtmlDocumentHandler} for this builder. |
| * |
| * @param documentHandler |
| * the document handler |
| * @see HtmlDocumentHandler |
| */ |
| public void setDocumentHandler(HtmlDocumentHandler documentHandler) { |
| this.documentHandler = checkNotNull(documentHandler, "Must provide a documentHandler"); //$NON-NLS-1$ |
| } |
| |
| private class DefaultDocumentHandler implements HtmlDocumentHandler { |
| |
| @Override |
| public void beginDocument(HtmlDocumentBuilder builder, XmlStreamWriter writer) { |
| if (emitAsDocument) { |
| if (encoding != null && encoding.length() > 0) { |
| writer.writeStartDocument(encoding, "1.0"); //$NON-NLS-1$ |
| } else { |
| writer.writeStartDocument(); |
| } |
| |
| if (emitDtd && htmlDtd != null) { |
| writer.writeDTD(htmlDtd); |
| } |
| |
| if (copyrightNotice != null) { |
| writer.writeComment(copyrightNotice); |
| } |
| |
| writer.writeStartElement(htmlNsUri, "html"); //$NON-NLS-1$ |
| writer.writeDefaultNamespace(htmlNsUri); |
| |
| emitHead(); |
| beginBody(); |
| } else { |
| // sanity check |
| if (stylesheets != null && !stylesheets.isEmpty()) { |
| throw new IllegalStateException(Messages.getString("HtmlDocumentBuilder.0")); //$NON-NLS-1$ |
| } |
| } |
| } |
| |
| @Override |
| public void endDocument(HtmlDocumentBuilder builder, XmlStreamWriter writer) { |
| if (emitAsDocument) { |
| endBody(); |
| writer.writeEndElement(); // html |
| writer.writeEndDocument(); |
| } |
| } |
| } |
| |
| @Override |
| public void beginDocument() { |
| writer.setDefaultNamespace(htmlNsUri); |
| documentHandler.beginDocument(this, writer); |
| } |
| |
| /** |
| * Emit the HTML head, including the head tag itself. |
| * |
| * @see #emitHeadContents() |
| */ |
| protected void emitHead() { |
| writer.writeStartElement(htmlNsUri, "head"); //$NON-NLS-1$ |
| emitHeadContents(); |
| writer.writeEndElement(); // head |
| } |
| |
| /** |
| * emit the contents of the HTML head, excluding the head tag itself. Subclasses may override to change the contents |
| * of the head. Subclasses should consider calling <code>super.emitHeadContents()</code> in order to preserve |
| * features such as emitting the base, title and stylesheets. |
| * |
| * @see #emitHead() |
| */ |
| protected void emitHeadContents() { |
| if (encoding != null && encoding.length() > 0) { |
| // bug 259786: add the charset as a HTML meta http-equiv |
| // see http://www.w3.org/International/tutorials/tutorial-char-enc/ |
| // |
| // <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> |
| writer.writeEmptyElement(htmlNsUri, "meta"); //$NON-NLS-1$ |
| writer.writeAttribute("http-equiv", "Content-Type"); //$NON-NLS-1$ //$NON-NLS-2$ |
| writer.writeAttribute("content", String.format("text/html; charset=%s", encoding)); //$NON-NLS-1$//$NON-NLS-2$ |
| } |
| if (copyrightNotice != null) { |
| writer.writeEmptyElement(htmlNsUri, "meta"); //$NON-NLS-1$ |
| writer.writeAttribute("name", "copyright"); //$NON-NLS-1$ //$NON-NLS-2$ |
| writer.writeAttribute("content", copyrightNotice); //$NON-NLS-1$ |
| } |
| if (base != null && baseInHead) { |
| writer.writeEmptyElement(htmlNsUri, "base"); //$NON-NLS-1$ |
| writer.writeAttribute("href", base.toString()); //$NON-NLS-1$ |
| } |
| if (title != null) { |
| writer.writeStartElement(htmlNsUri, "title"); //$NON-NLS-1$ |
| writer.writeCharacters(title); |
| writer.writeEndElement(); // title |
| } |
| if (!useInlineStyles && !suppressBuiltInStyles) { |
| writer.writeStartElement(htmlNsUri, "style"); //$NON-NLS-1$ |
| writer.writeAttribute("type", "text/css"); //$NON-NLS-1$ //$NON-NLS-2$ |
| writer.writeCharacters("\n"); //$NON-NLS-1$ |
| for (Entry<BlockType, ElementInfo> ent : blockTypeToElementInfo.entrySet()) { |
| ElementInfo elementInfo = ent.getValue(); |
| while (elementInfo != null) { |
| if (elementInfo.cssStyles != null && elementInfo.cssClass != null) { |
| String[] classes = elementInfo.cssClass.split("\\s+"); //$NON-NLS-1$ |
| for (String cssClass : classes) { |
| writer.writeCharacters("."); //$NON-NLS-1$ |
| writer.writeCharacters(cssClass); |
| writer.writeCharacters(" "); //$NON-NLS-1$ |
| } |
| writer.writeCharacters("{"); //$NON-NLS-1$ |
| writer.writeCharacters(elementInfo.cssStyles); |
| writer.writeCharacters("}\n"); //$NON-NLS-1$ |
| } |
| elementInfo = elementInfo.next; |
| } |
| } |
| writer.writeEndElement(); |
| } |
| if (stylesheets != null) { |
| for (Stylesheet stylesheet : stylesheets) { |
| emitStylesheet(stylesheet); |
| } |
| } |
| } |
| |
| private void emitStylesheet(Stylesheet stylesheet) { |
| if (stylesheet.url != null) { |
| // <link type="text/css" rel="stylesheet" href="url"/> |
| writer.writeEmptyElement(htmlNsUri, "link"); //$NON-NLS-1$ |
| writer.writeAttribute("type", "text/css"); //$NON-NLS-1$ //$NON-NLS-2$ |
| writer.writeAttribute("rel", "stylesheet"); //$NON-NLS-1$ //$NON-NLS-2$ |
| writer.writeAttribute("href", makeUrlAbsolute(stylesheet.url)); //$NON-NLS-1$ |
| for (Entry<String, String> attr : stylesheet.attributes.entrySet()) { |
| String attrName = attr.getKey(); |
| if (!"type".equals(attrName) && !"rel".equals(attrName) && !"href".equals(attrName)) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ |
| writer.writeAttribute(attrName, attr.getValue()); |
| } |
| } |
| } else { |
| // <style type="text/css"> |
| // ... contents of the file ... |
| // </style> |
| writer.writeStartElement(htmlNsUri, "style"); //$NON-NLS-1$ |
| writer.writeAttribute("type", "text/css"); //$NON-NLS-1$ //$NON-NLS-2$ |
| for (Entry<String, String> attr : stylesheet.attributes.entrySet()) { |
| String attrName = attr.getKey(); |
| if (!"type".equals(attrName)) { //$NON-NLS-1$ |
| writer.writeAttribute(attrName, attr.getValue()); |
| } |
| } |
| |
| String css; |
| if (stylesheet.file != null) { |
| try { |
| css = readFully(stylesheet.file); |
| } catch (IOException e) { |
| throw new IllegalStateException(MessageFormat.format(Messages.getString("HtmlDocumentBuilder.4"), //$NON-NLS-1$ |
| stylesheet.file), e); |
| } |
| } else { |
| try { |
| css = readFully(stylesheet.reader, 1024); |
| } catch (IOException e) { |
| throw new IllegalStateException(Messages.getString("HtmlDocumentBuilder.5"), e); //$NON-NLS-1$ |
| } |
| } |
| writer.writeCharacters(css); |
| writer.writeEndElement(); |
| } |
| } |
| |
| @Override |
| public void endDocument() { |
| documentHandler.endDocument(this, writer); |
| writer.close(); |
| } |
| |
| /** |
| * begin the body by emitting the body element. Overriding methods should call <code>super.beginBody()</code>. |
| * |
| * @see #endBody() |
| */ |
| protected void beginBody() { |
| writer.writeStartElement(htmlNsUri, "body"); //$NON-NLS-1$ |
| } |
| |
| /** |
| * end the body by emitting the body end element tag. Overriding methods should call <code>super.endBody()</code>. |
| * |
| * @see #beginBody() |
| */ |
| protected void endBody() { |
| writer.writeEndElement(); // body |
| } |
| |
| @Override |
| public void entityReference(String entity) { |
| if (filterEntityReferences && !entity.isEmpty()) { |
| if (entity.charAt(0) == '#') { |
| writer.writeEntityRef(entity); |
| } else { |
| List<String> emitEntity = HtmlEntities.instance().nameToEntityReferences(entity); |
| if (emitEntity.isEmpty()) { |
| writer.writeCharacters("&"); //$NON-NLS-1$ |
| writer.writeCharacters(entity); |
| writer.writeCharacters(";"); //$NON-NLS-1$ |
| } else { |
| for (String numericEntity : emitEntity) { |
| writer.writeEntityRef(numericEntity); |
| } |
| } |
| } |
| } else { |
| writer.writeEntityRef(entity); |
| } |
| } |
| |
| @Override |
| public void acronym(String text, String definition) { |
| writer.writeStartElement(htmlNsUri, "acronym"); //$NON-NLS-1$ |
| writer.writeAttribute("title", definition); //$NON-NLS-1$ |
| writer.writeCharacters(text); |
| writer.writeEndElement(); |
| } |
| |
| @Override |
| public void link(Attributes attributes, String hrefOrHashName, String text) { |
| writer.writeStartElement(htmlNsUri, spanTypeToElementName.get(SpanType.LINK)); |
| emitAnchorHref(hrefOrHashName); |
| applyLinkAttributes(attributes, hrefOrHashName); |
| characters(text); |
| writer.writeEndElement(); |
| } |
| |
| @Override |
| public void beginBlock(BlockType type, Attributes attributes) { |
| ElementInfo elementInfo = blockTypeToElementInfo.get(type); |
| if (elementInfo == null) { |
| throw new IllegalStateException(type.name()); |
| } |
| writeBlockElements(attributes, elementInfo); |
| blockState.push(elementInfo); |
| if (type == BlockType.TABLE) { |
| applyTableAttributes(attributes); |
| } else if (type == BlockType.TABLE_ROW) { |
| applyTableRowAttributes(attributes); |
| } else if (type == BlockType.TABLE_CELL_HEADER || type == BlockType.TABLE_CELL_NORMAL) { |
| applyCellAttributes(attributes); |
| } else if (type == BlockType.BULLETED_LIST || type == BlockType.NUMERIC_LIST) { |
| applyListAttributes(attributes); |
| } else if (type == BlockType.QUOTE) { |
| applyQuoteAttributes(attributes); |
| } else { |
| applyAttributes(attributes); |
| |
| // create the titled panel effect if a title is specified |
| if (attributes.getTitle() != null) { |
| beginBlock(BlockType.PARAGRAPH, new Attributes()); |
| beginSpan(SpanType.BOLD, new Attributes()); |
| characters(attributes.getTitle()); |
| endSpan(); |
| endBlock(); |
| } |
| } |
| } |
| |
| private void writeBlockElements(Attributes attributes, ElementInfo elementInfo) { |
| writer.writeStartElement(htmlNsUri, elementInfo.name); |
| String originalCssClasses = attributes.getCssClass(); |
| if (elementInfo.cssClass != null) { |
| attributes.appendCssClass(elementInfo.cssClass); |
| } |
| if (useInlineStyles && !suppressBuiltInStyles && elementInfo.cssStyles != null) { |
| attributes.appendCssStyle(elementInfo.cssStyles); |
| } |
| if (elementInfo.next != null) { |
| if (originalCssClasses != null) { |
| writer.writeAttribute("class", originalCssClasses); //$NON-NLS-1$ |
| } |
| |
| Attributes childAttributes = new Attributes(); |
| childAttributes.setCssClass(originalCssClasses); |
| writeBlockElements(childAttributes, elementInfo.next); |
| } |
| } |
| |
| @Override |
| public void beginHeading(int level, Attributes attributes) { |
| if (level > 6) { |
| level = 6; |
| } |
| writer.writeStartElement(htmlNsUri, "h" + level); //$NON-NLS-1$ |
| applyAttributes(attributes); |
| } |
| |
| @Override |
| public void beginSpan(SpanType type, Attributes attributes) { |
| String elementName = spanTypeToElementName.get(type); |
| if (elementName == null) { |
| throw new IllegalStateException(type.name()); |
| } |
| writer.writeStartElement(htmlNsUri, elementName); |
| if (type == SpanType.LINK && attributes instanceof LinkAttributes) { |
| String href = ((LinkAttributes) attributes).getHref(); |
| emitAnchorHref(href); |
| applyLinkAttributes(attributes, href); |
| } else { |
| applyAttributes(attributes); |
| } |
| } |
| |
| @Override |
| public void endBlock() { |
| ElementInfo elementInfo = blockState.pop(); |
| for (int x = 0; x < elementInfo.size(); ++x) { |
| writer.writeEndElement(); |
| } |
| } |
| |
| @Override |
| public void endHeading() { |
| writer.writeEndElement(); |
| } |
| |
| @Override |
| public void endSpan() { |
| writer.writeEndElement(); |
| } |
| |
| @Override |
| public void image(Attributes attributes, String url) { |
| writer.writeEmptyElement(htmlNsUri, "img"); //$NON-NLS-1$ |
| applyImageAttributes(attributes); |
| url = prependImageUrl(url); |
| writer.writeAttribute("src", makeUrlAbsolute(url)); //$NON-NLS-1$ |
| } |
| |
| private void applyListAttributes(Attributes attributes) { |
| applyAttributes(attributes); |
| if (attributes instanceof ListAttributes) { |
| ListAttributes listAttributes = (ListAttributes) attributes; |
| if (listAttributes.getStart() != null) { |
| writer.writeAttribute("start", listAttributes.getStart()); //$NON-NLS-1$ |
| } |
| } |
| } |
| |
| private void applyQuoteAttributes(Attributes attributes) { |
| applyAttributes(attributes); |
| if (attributes instanceof QuoteAttributes) { |
| QuoteAttributes quoteAttributes = (QuoteAttributes) attributes; |
| if (quoteAttributes.getCitation() != null) { |
| writer.writeAttribute("cite", quoteAttributes.getCitation()); //$NON-NLS-1$ |
| } |
| } |
| } |
| |
| private void applyTableAttributes(Attributes attributes) { |
| applyAttributes(attributes); |
| if (attributes.getTitle() != null) { |
| writer.writeAttribute("title", attributes.getTitle()); //$NON-NLS-1$ |
| } |
| if (attributes instanceof TableAttributes) { |
| TableAttributes tableAttributes = (TableAttributes) attributes; |
| if (tableAttributes.getBgcolor() != null) { |
| writer.writeAttribute("bgcolor", tableAttributes.getBgcolor()); //$NON-NLS-1$ |
| } |
| if (tableAttributes.getBorder() != null) { |
| writer.writeAttribute("border", tableAttributes.getBorder()); //$NON-NLS-1$ |
| } |
| if (tableAttributes.getCellpadding() != null) { |
| writer.writeAttribute("cellpadding", tableAttributes.getCellpadding()); //$NON-NLS-1$ |
| } |
| if (tableAttributes.getCellspacing() != null) { |
| writer.writeAttribute("cellspacing", tableAttributes.getCellspacing()); //$NON-NLS-1$ |
| } |
| if (tableAttributes.getFrame() != null) { |
| writer.writeAttribute("frame", tableAttributes.getFrame()); //$NON-NLS-1$ |
| } |
| if (tableAttributes.getRules() != null) { |
| writer.writeAttribute("rules", tableAttributes.getRules()); //$NON-NLS-1$ |
| } |
| if (tableAttributes.getSummary() != null) { |
| writer.writeAttribute("summary", tableAttributes.getSummary()); //$NON-NLS-1$ |
| } |
| if (tableAttributes.getWidth() != null) { |
| writer.writeAttribute("width", tableAttributes.getWidth()); //$NON-NLS-1$ |
| } |
| } |
| } |
| |
| private void applyTableRowAttributes(Attributes attributes) { |
| applyAttributes(attributes); |
| if (attributes.getTitle() != null) { |
| writer.writeAttribute("title", attributes.getTitle()); //$NON-NLS-1$ |
| } |
| if (attributes instanceof TableRowAttributes) { |
| TableRowAttributes tableRowAttributes = (TableRowAttributes) attributes; |
| if (tableRowAttributes.getBgcolor() != null) { |
| writer.writeAttribute("bgcolor", tableRowAttributes.getBgcolor()); //$NON-NLS-1$ |
| } |
| if (tableRowAttributes.getAlign() != null) { |
| writer.writeAttribute("align", tableRowAttributes.getAlign()); //$NON-NLS-1$ |
| } |
| if (tableRowAttributes.getValign() != null) { |
| writer.writeAttribute("valign", tableRowAttributes.getValign()); //$NON-NLS-1$ |
| } |
| } |
| } |
| |
| private void applyCellAttributes(Attributes attributes) { |
| applyAttributes(attributes); |
| if (attributes.getTitle() != null) { |
| writer.writeAttribute("title", attributes.getTitle()); //$NON-NLS-1$ |
| } |
| |
| if (attributes instanceof TableCellAttributes) { |
| TableCellAttributes tableCellAttributes = (TableCellAttributes) attributes; |
| if (tableCellAttributes.getBgcolor() != null) { |
| writer.writeAttribute("bgcolor", tableCellAttributes.getBgcolor()); //$NON-NLS-1$ |
| } |
| if (tableCellAttributes.getAlign() != null) { |
| writer.writeAttribute("align", tableCellAttributes.getAlign()); //$NON-NLS-1$ |
| } |
| if (tableCellAttributes.getValign() != null) { |
| writer.writeAttribute("valign", tableCellAttributes.getValign()); //$NON-NLS-1$ |
| } |
| if (tableCellAttributes.getRowspan() != null) { |
| writer.writeAttribute("rowspan", tableCellAttributes.getRowspan()); //$NON-NLS-1$ |
| } |
| if (tableCellAttributes.getColspan() != null) { |
| writer.writeAttribute("colspan", tableCellAttributes.getColspan()); //$NON-NLS-1$ |
| } |
| } |
| } |
| |
| private void applyImageAttributes(Attributes attributes) { |
| int border = 0; |
| Align align = null; |
| if (attributes instanceof ImageAttributes) { |
| ImageAttributes imageAttributes = (ImageAttributes) attributes; |
| border = imageAttributes.getBorder(); |
| align = imageAttributes.getAlign(); |
| } |
| if (xhtmlStrict) { |
| String borderStyle = String.format("border-width: %spx;", border); //$NON-NLS-1$ |
| String alignStyle = null; |
| if (align != null) { |
| switch (align) { |
| case Center: |
| case Right: |
| case Left: |
| alignStyle = "text-align: " + align.name().toLowerCase() + ";"; //$NON-NLS-1$ //$NON-NLS-2$ |
| break; |
| case Bottom: |
| case Baseline: |
| case Top: |
| case Middle: |
| alignStyle = "vertical-align: " + align.name().toLowerCase() + ";"; //$NON-NLS-1$ //$NON-NLS-2$ |
| break; |
| case Texttop: |
| alignStyle = "vertical-align: text-top;"; //$NON-NLS-1$ |
| break; |
| case Absmiddle: |
| alignStyle = "vertical-align: middle;"; //$NON-NLS-1$ |
| break; |
| case Absbottom: |
| alignStyle = "vertical-align: bottom;"; //$NON-NLS-1$ |
| break; |
| } |
| } |
| String additionalStyles = borderStyle; |
| if (alignStyle != null) { |
| additionalStyles += alignStyle; |
| } |
| if (attributes.getCssStyle() == null || attributes.getCssStyle().length() == 0) { |
| attributes.setCssStyle(additionalStyles); |
| } else { |
| attributes.setCssStyle(additionalStyles + attributes.getCssStyle()); |
| } |
| } |
| applyAttributes(attributes); |
| boolean haveAlt = false; |
| |
| if (attributes instanceof ImageAttributes) { |
| ImageAttributes imageAttributes = (ImageAttributes) attributes; |
| if (imageAttributes.getHeight() != -1) { |
| String val = Integer.toString(imageAttributes.getHeight()); |
| if (imageAttributes.isHeightPercentage()) { |
| val += "%"; //$NON-NLS-1$ |
| } |
| writer.writeAttribute("height", val); //$NON-NLS-1$ |
| } |
| if (imageAttributes.getWidth() != -1) { |
| String val = Integer.toString(imageAttributes.getWidth()); |
| if (imageAttributes.isWidthPercentage()) { |
| val += "%"; //$NON-NLS-1$ |
| } |
| writer.writeAttribute("width", val); //$NON-NLS-1$ |
| } |
| if (!xhtmlStrict && align != null) { |
| writer.writeAttribute("align", align.name().toLowerCase()); //$NON-NLS-1$ |
| } |
| if (imageAttributes.getAlt() != null) { |
| haveAlt = true; |
| writer.writeAttribute("alt", imageAttributes.getAlt()); //$NON-NLS-1$ |
| } |
| } |
| if (attributes.getTitle() != null) { |
| writer.writeAttribute("title", attributes.getTitle()); //$NON-NLS-1$ |
| if (!haveAlt) { |
| haveAlt = true; |
| writer.writeAttribute("alt", attributes.getTitle()); //$NON-NLS-1$ |
| } |
| } |
| if (xhtmlStrict) { |
| if (!haveAlt) { |
| // XHTML requires img/@alt |
| writer.writeAttribute("alt", ""); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| } else { |
| // only specify border attribute if it's not already specified in CSS |
| writer.writeAttribute("border", Integer.toString(border)); //$NON-NLS-1$ |
| } |
| } |
| |
| private void applyLinkAttributes(Attributes attributes, String href) { |
| applyAttributes(attributes); |
| boolean hasTarget = false; |
| String rel = linkRel; |
| if (attributes instanceof LinkAttributes) { |
| LinkAttributes linkAttributes = (LinkAttributes) attributes; |
| if (linkAttributes.getTarget() != null) { |
| hasTarget = true; |
| writer.writeAttribute("target", linkAttributes.getTarget()); //$NON-NLS-1$ |
| } |
| if (linkAttributes.getRel() != null) { |
| rel = rel == null ? linkAttributes.getRel() : linkAttributes.getRel() + ' ' + rel; |
| } |
| |
| } |
| if (attributes.getTitle() != null && attributes.getTitle().length() > 0) { |
| writer.writeAttribute("title", attributes.getTitle()); //$NON-NLS-1$ |
| } |
| if (!hasTarget && defaultAbsoluteLinkTarget != null && href != null) { |
| if (isExternalLink(href)) { |
| writer.writeAttribute("target", defaultAbsoluteLinkTarget); //$NON-NLS-1$ |
| } |
| } |
| |
| if (rel != null) { |
| writer.writeAttribute("rel", rel); //$NON-NLS-1$ |
| } |
| } |
| |
| /** |
| * Note: this method does not apply the {@link Attributes#getTitle() title}. |
| */ |
| private void applyAttributes(Attributes attributes) { |
| if (attributes.getId() != null) { |
| writer.writeAttribute("id", attributes.getId()); //$NON-NLS-1$ |
| } |
| if (attributes.getCssClass() != null) { |
| writer.writeAttribute("class", attributes.getCssClass()); //$NON-NLS-1$ |
| } |
| if (attributes.getCssStyle() != null) { |
| writer.writeAttribute("style", attributes.getCssStyle()); //$NON-NLS-1$ |
| } |
| if (attributes.getLanguage() != null) { |
| writer.writeAttribute("lang", attributes.getLanguage()); //$NON-NLS-1$ |
| } |
| } |
| |
| @Override |
| public void imageLink(Attributes linkAttributes, Attributes imageAttributes, String href, String imageUrl) { |
| writer.writeStartElement(htmlNsUri, "a"); //$NON-NLS-1$ |
| emitAnchorHref(href); |
| applyLinkAttributes(linkAttributes, href); |
| writer.writeEmptyElement(htmlNsUri, "img"); //$NON-NLS-1$ |
| applyImageAttributes(imageAttributes); |
| imageUrl = prependImageUrl(imageUrl); |
| writer.writeAttribute("src", makeUrlAbsolute(imageUrl)); //$NON-NLS-1$ |
| writer.writeEndElement(); // a |
| } |
| |
| /** |
| * emit the href attribute of an anchor. Subclasses may override to alter the default href or to add other |
| * attributes such as <code>onclick</code>. Overriding classes should pass the href to |
| * {@link #makeUrlAbsolute(String)} prior to writing it to the writer. |
| * |
| * @param href |
| * the url for the href attribute |
| * @see #getHtmlFilenameFormat() |
| */ |
| protected void emitAnchorHref(String href) { |
| if (href != null) { |
| writer.writeAttribute("href", makeUrlAbsolute(applyHtmlFilenameFormat(href))); //$NON-NLS-1$ |
| } |
| } |
| |
| /** |
| * Applies the {@link #getHtmlFilenameFormat() HTML filename format} to links that are missing a filename extension |
| * using the format specified by {@link #getHtmlFilenameFormat()}. |
| * |
| * @param href |
| * the link |
| * @return the given {@code href} with the {@link #getHtmlFilenameFormat() HTML filename format} applied, or the |
| * original {@code href} if the {@link #getHtmlFilenameFormat()} is null |
| * @see #getHtmlFilenameFormat() |
| */ |
| private String applyHtmlFilenameFormat(String href) { |
| if (getHtmlFilenameFormat() != null) { |
| if (isMissingFilenameExtension(href) && !isAbsoluteUrl(href)) { |
| int indexOfHash = href.indexOf('#'); |
| if (indexOfHash > 0) { |
| href = getHtmlFilenameFormat().replace("$1", href.substring(0, indexOfHash)) //$NON-NLS-1$ |
| + href.substring(indexOfHash); |
| } else if (indexOfHash == -1) { |
| href = getHtmlFilenameFormat().replace("$1", href); //$NON-NLS-1$ |
| } |
| } |
| } |
| return href; |
| } |
| |
| private boolean isAbsoluteUrl(String href) { |
| return ABSOLUTE_URL_PATTERN.matcher(href).matches(); |
| } |
| |
| /** |
| * Determines whether or not the {@code href} has a a filename extension |
| * |
| * @param href |
| * the reference to test |
| * @return {@code true} if the {@code href} is relative and missing a filename extension, otherwise {@code false} |
| */ |
| private boolean isMissingFilenameExtension(String href) { |
| int lasIndexOfSlash = href.lastIndexOf('/'); |
| return href.lastIndexOf('.') <= lasIndexOfSlash && lasIndexOfSlash < href.length() - 1; |
| } |
| |
| /** |
| * Provides the HTML filename format which is used to rewrite relative URLs having no filename extension. Specifying |
| * the HTML filename format enables content to have relative hyperlinks to generated files without having to specify |
| * the filename extension in the hyperlink. If specified, the returned value is a pattern where "$1" indicates the |
| * location of the filename. For example "$1.html". The default value is {@code null}. |
| * |
| * @see #setHtmlFilenameFormat(String) |
| * @return the HTML filename format or {@code null} |
| */ |
| public String getHtmlFilenameFormat() { |
| return htmlFilenameFormat; |
| } |
| |
| /** |
| * Sets the HTML filename format which is used to rewrite relative URLs having no filename extension. Specifying the |
| * HTML filename format enables content to have relative hyperlinks to generated files without having to specify the |
| * filename extension in the hyperlink. If specified, the returned value is a pattern where "$1" indicates the |
| * location of the filename. For example "$1.html". The default value is {@code null}. |
| * |
| * @param htmlFilenameFormat |
| * the HTML filename format or <code>null</code> |
| * @see #getHtmlFilenameFormat() |
| */ |
| public void setHtmlFilenameFormat(String htmlFilenameFormat) { |
| checkArgument(htmlFilenameFormat == null || htmlFilenameFormat.contains("$1"), //$NON-NLS-1$ |
| "The HTML filename format must contain \"$1\""); //$NON-NLS-1$ |
| this.htmlFilenameFormat = htmlFilenameFormat; |
| } |
| |
| private String prependImageUrl(String imageUrl) { |
| if (prependImagePrefix == null || prependImagePrefix.length() == 0) { |
| return imageUrl; |
| } |
| if (isAbsoluteUrl(imageUrl) || imageUrl.contains("../")) { //$NON-NLS-1$ |
| return imageUrl; |
| } |
| String url = prependImagePrefix; |
| if (!prependImagePrefix.endsWith("/")) { //$NON-NLS-1$ |
| url += '/'; |
| } |
| url += imageUrl; |
| return url; |
| } |
| |
| @Override |
| public void lineBreak() { |
| writer.writeEmptyElement(htmlNsUri, "br"); //$NON-NLS-1$ |
| } |
| |
| /** |
| * |
| */ |
| @Override |
| public void horizontalRule() { |
| writer.writeEmptyElement(htmlNsUri, "hr"); //$NON-NLS-1$ |
| } |
| |
| @Override |
| public void charactersUnescaped(String literal) { |
| writer.writeLiteral(literal); |
| } |
| |
| private static final class ElementInfo { |
| final String name; |
| |
| final String cssClass; |
| |
| final String cssStyles; |
| |
| final ElementInfo next; |
| |
| public ElementInfo(String name, String cssClass, String cssStyles) { |
| this(name, cssClass, cssStyles, null); |
| } |
| |
| public ElementInfo(String name, String cssClass, String cssStyles, ElementInfo next) { |
| this.name = name; |
| this.cssClass = cssClass; |
| this.cssStyles = cssStyles != null && !cssStyles.endsWith(";") ? cssStyles + ';' : cssStyles; //$NON-NLS-1$ |
| this.next = next; |
| } |
| |
| public ElementInfo(String name) { |
| this(name, null, null); |
| } |
| |
| public int size() { |
| return 1 + (next == null ? 0 : next.size()); |
| } |
| } |
| |
| /** |
| * A CSS stylesheet definition, created via one of {@link HtmlDocumentBuilder#addCssStylesheet(File)} or |
| * {@link HtmlDocumentBuilder#addCssStylesheet(String)}. |
| */ |
| public static class Stylesheet { |
| private final String url; |
| |
| private final File file; |
| |
| private final Reader reader; |
| |
| private final Map<String, String> attributes = new HashMap<String, String>(); |
| |
| /** |
| * Create a CSS stylesheet where the contents of the CSS stylesheet are embedded in the HTML. Generates code |
| * similar to the following: |
| * |
| * <pre> |
| * <code> |
| * <style type="text/css"> |
| * ... contents of the file ... |
| * </style> |
| * </code> |
| * </pre> |
| * |
| * @param file |
| * the CSS file whose contents must be available |
| */ |
| public Stylesheet(File file) { |
| if (file == null) { |
| throw new IllegalArgumentException(); |
| } |
| this.file = file; |
| url = null; |
| reader = null; |
| } |
| |
| /** |
| * Create a CSS stylesheet to the output document as an URL where the CSS stylesheet is referenced as an HTML |
| * link. Calling this method after {@link #beginDocument() starting the document} has no effect. Generates code |
| * similar to the following: |
| * |
| * <pre> |
| * <link type="text/css" rel="stylesheet" href="url"/> |
| * </pre> |
| * |
| * @param url |
| * the CSS url to use, which may be relative or absolute |
| */ |
| public Stylesheet(String url) { |
| if (url == null || url.length() == 0) { |
| throw new IllegalArgumentException(); |
| } |
| this.url = url; |
| file = null; |
| reader = null; |
| } |
| |
| /** |
| * Create a CSS stylesheet where the contents of the CSS stylesheet are embedded in the HTML. Generates code |
| * similar to the following: |
| * |
| * <pre> |
| * <code> |
| * <style type="text/css"> |
| * ... contents of the file ... |
| * </style> |
| * </code> |
| * </pre> |
| * |
| * The caller is responsible for closing the reader. |
| * |
| * @param reader |
| * the reader from which content is provided. |
| */ |
| public Stylesheet(Reader reader) { |
| if (reader == null) { |
| throw new IllegalArgumentException(); |
| } |
| this.reader = reader; |
| file = null; |
| url = null; |
| } |
| |
| /** |
| * the attributes of the stylesheet, which may be modified prior to adding to the document. Attributes |
| * <code>href</code>, <code>type</code> and <code>rel</code> are all ignored. |
| */ |
| public Map<String, String> getAttributes() { |
| return attributes; |
| } |
| |
| /** |
| * the file of the stylesheet, or null if it's not defined |
| */ |
| public File getFile() { |
| return file; |
| } |
| |
| /** |
| * the url of the stylesheet, or null if it's not defined |
| */ |
| public String getUrl() { |
| return url; |
| } |
| |
| /** |
| * the content reader, or null if it's not defined. |
| */ |
| public Reader getReader() { |
| return reader; |
| } |
| } |
| |
| private String readFully(File inputFile) throws IOException { |
| int length = (int) inputFile.length(); |
| if (length <= 0) { |
| length = 2048; |
| } |
| return readFully(getReader(inputFile), length); |
| } |
| |
| private String readFully(Reader input, int bufferSize) throws IOException { |
| StringBuilder buf = new StringBuilder(bufferSize); |
| try { |
| Reader reader = new BufferedReader(input); |
| int c; |
| while ((c = reader.read()) != -1) { |
| buf.append((char) c); |
| } |
| } finally { |
| input.close(); |
| } |
| return buf.toString(); |
| } |
| |
| protected Reader getReader(File inputFile) throws FileNotFoundException { |
| return new FileReader(inputFile); |
| } |
| |
| /** |
| * if specified, the prefix is prepended to relative image urls. |
| */ |
| public void setPrependImagePrefix(String prependImagePrefix) { |
| this.prependImagePrefix = prependImagePrefix; |
| } |
| |
| /** |
| * if specified, the prefix is prepended to relative image urls. |
| */ |
| public String getPrependImagePrefix() { |
| return prependImagePrefix; |
| } |
| |
| /** |
| * Indicates that {@link #entityReference(String) entity references} should be filtered. Defaults to false. When |
| * filtered, known HTML entity references are converted to their numeric counterpart, and unknown entity references |
| * are emitted as plain text. |
| * |
| * @see <a href="http://www.w3schools.com/tags/ref_entities.asp">HTML Entity Reference</a> |
| */ |
| public boolean isFilterEntityReferences() { |
| return filterEntityReferences; |
| } |
| |
| /** |
| * Indicates that {@link #entityReference(String) entity references} should be filtered. Defaults to false. When |
| * filtered, known HTML entity references are converted to their numeric counterpart, and unknown entity references |
| * are emitted as plain text. |
| * |
| * @see <a href="http://www.w3schools.com/tags/ref_entities.asp">HTML Entity Reference</a> |
| */ |
| public void setFilterEntityReferences(boolean filterEntityReferences) { |
| this.filterEntityReferences = filterEntityReferences; |
| } |
| |
| /** |
| * the copyright notice that should appear in the generated output |
| */ |
| public String getCopyrightNotice() { |
| return copyrightNotice; |
| } |
| |
| /** |
| * the copyright notice that should appear in the generated output |
| * |
| * @param copyrightNotice |
| * the notice, or null if there should be none |
| */ |
| public void setCopyrightNotice(String copyrightNotice) { |
| this.copyrightNotice = copyrightNotice; |
| } |
| } |