blob: 4d919be696d18e19d01089ce8bb7a267139e4d32 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2007, 2013 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
*******************************************************************************/
package org.eclipse.mylyn.wikitext.parser.builder;
import java.io.Writer;
import java.util.HashSet;
import java.util.Set;
import java.util.Stack;
import java.util.logging.Logger;
import org.eclipse.mylyn.wikitext.parser.Attributes;
import org.eclipse.mylyn.wikitext.parser.ImageAttributes;
import org.eclipse.mylyn.wikitext.parser.LinkAttributes;
import org.eclipse.mylyn.wikitext.parser.outline.OutlineItem;
import org.eclipse.mylyn.wikitext.util.FormattingXMLStreamWriter;
import org.eclipse.mylyn.wikitext.util.XmlStreamWriter;
/**
* A document builder that creates an OASIS DITA topic
*
* @author David Green
* @see DitaBookMapDocumentBuilder
* @since 3.0
*/
public class DitaTopicDocumentBuilder extends AbstractXmlDocumentBuilder {
private static final String __TOPIC = "__topic"; //$NON-NLS-1$
private static Set<Integer> entityReferenceToUnicode = new HashSet<Integer>();
static {
entityReferenceToUnicode.add(215);
entityReferenceToUnicode.add(8211);
entityReferenceToUnicode.add(8212);
entityReferenceToUnicode.add(8220);
entityReferenceToUnicode.add(8221);
entityReferenceToUnicode.add(8216);
entityReferenceToUnicode.add(8217);
}
private final Stack<BlockDescription> blockDescriptions = new Stack<BlockDescription>();
private String doctype = "<!DOCTYPE topic PUBLIC \"-//OASIS//DTD DITA 1.1 Topic//EN\" \"http://docs.oasis-open.org/dita/v1.1/OS/dtd/topic.dtd\">"; //$NON-NLS-1$
private static class TopicInfo {
int headingLevel;
int openElements;
}
private final Stack<TopicInfo> topicInfos = new Stack<TopicInfo>();
private OutlineItem outline;
private String filename;
private int topicBreakLevel = Integer.MAX_VALUE;
private String rootTitle;
/**
* Create a DitaTopicDocumentBuilder that writes formatted output to the given writer. Output without formatting can
* be created using {@link #DitaTopicDocumentBuilder(XmlStreamWriter, boolean)}.
*
* @param out
* the writer to which formatted XML content output
*/
public DitaTopicDocumentBuilder(Writer out) {
super(out);
}
/**
* Equivalent to <code>new DitaTopicDocumentBuilder(writer,true)</code>
*
* @see #DitaTopicDocumentBuilder(XmlStreamWriter, boolean)
*/
public DitaTopicDocumentBuilder(XmlStreamWriter writer) {
this(writer, true);
}
/**
* @param writer
* the writer to which output is written
* @param formatting
* indicate if the writer should format output
*/
public DitaTopicDocumentBuilder(XmlStreamWriter writer, boolean formatting) {
super(formatting ? wrapStreamWriter(writer) : writer);
}
@Override
protected XmlStreamWriter createXmlStreamWriter(Writer out) {
XmlStreamWriter writer = super.createXmlStreamWriter(out);
return wrapStreamWriter(writer);
}
/**
* wrap the stream writer in order to produce formatted output
*/
private static FormattingXMLStreamWriter wrapStreamWriter(XmlStreamWriter writer) {
return new FormattingXMLStreamWriter(writer) {
@Override
protected boolean preserveWhitespace(String elementName) {
return elementName.equals("codeblock") || elementName.startsWith("pre"); //$NON-NLS-1$ //$NON-NLS-2$
}
};
}
public void setDoctype(String doctype) {
this.doctype = doctype;
}
public String getDoctype() {
return doctype;
}
@Override
public void acronym(String text, String definition) {
ensureOpenTopic();
// TODO: definition? according to DITA 1.1 'term' is the right thing to use here, however DITA 1.1 has no provision for a glossary.
// we may want to look at reference/refbody/simpletable to generate a glossary
writer.writeStartElement("term"); //$NON-NLS-1$
characters(text);
writer.writeEndElement();
}
private BlockDescription findBlockDescription(BlockType type) {
for (int x = blockDescriptions.size() - 1; x >= 0; --x) {
BlockDescription blockDescription = blockDescriptions.get(x);
if (blockDescription.type == type) {
return blockDescription;
}
}
return null;
}
private static class BlockDescription {
BlockType type;
int size;
int entrySize; // the size of an entry, if it is open, otherwise 0
@SuppressWarnings("unused")
final String[] nestedElementNames;
final boolean closeElementsOnBlockStart;
public BlockDescription(BlockType type, int size, String[] nestedElementNames,
boolean closeElementsOnBlockStart) {
this.size = size;
this.entrySize = nestedElementNames == null ? 0 : nestedElementNames.length;
this.type = type;
this.nestedElementNames = nestedElementNames;
this.closeElementsOnBlockStart = closeElementsOnBlockStart;
}
}
@Override
public void beginBlock(BlockType type, Attributes attributes) {
ensureOpenTopic();
String elementName;
String[] elementNames = null;
boolean allowTitle = false;
boolean closeElementsOnBlockStart = false;
BlockDescription previousBlock = null;
if (!blockDescriptions.isEmpty()) {
previousBlock = blockDescriptions.peek();
}
boolean phraseTitle = false;
switch (type) {
case BULLETED_LIST:
elementName = "ul"; //$NON-NLS-1$
break;
case NUMERIC_LIST:
elementName = "ol"; //$NON-NLS-1$
break;
case DEFINITION_LIST:
elementName = "dl"; //$NON-NLS-1$
break;
case DEFINITION_TERM:
BlockDescription blockDescription = findBlockDescription(BlockType.DEFINITION_LIST);
if (blockDescription.entrySize > 0) {
endBlockEntry(blockDescription);
}
openBlockEntry(blockDescription, new String[] { "dlentry" }); //$NON-NLS-1$
elementName = "dt"; //$NON-NLS-1$
break;
case DEFINITION_ITEM:
elementName = "dd"; //$NON-NLS-1$
elementNames = new String[] { "p" }; //$NON-NLS-1$
closeElementsOnBlockStart = true;
break;
case FOOTNOTE:
case PARAGRAPH:
elementName = "p"; //$NON-NLS-1$
break;
case CODE:
elementName = "pre"; //$NON-NLS-1$
elementNames = new String[] { "codeph" }; //$NON-NLS-1$
break;
case PREFORMATTED:
elementName = "pre"; //$NON-NLS-1$
break;
case QUOTE:
elementName = "lq"; //$NON-NLS-1$
break;
case LIST_ITEM:
elementName = "li"; //$NON-NLS-1$
elementNames = new String[] { "p" }; //$NON-NLS-1$
closeElementsOnBlockStart = true;
break;
case TABLE:
elementName = "simpletable"; //$NON-NLS-1$
break;
case TABLE_CELL_HEADER:
// TODO: no such thing as header cells in DITA, only header rows
// need a way to detect beforehand if we're about to emit a header row
elementName = "stentry"; //$NON-NLS-1$
break;
case TABLE_CELL_NORMAL:
elementName = "stentry"; //$NON-NLS-1$
break;
case TABLE_ROW:
elementName = "strow"; //$NON-NLS-1$
break;
case INFORMATION:
case NOTE:
case WARNING:
case TIP:
case PANEL:
elementName = "note"; //$NON-NLS-1$
allowTitle = true;
phraseTitle = true;
break;
case DIV:
elementName = null;
break;
default:
throw new IllegalStateException(type.name());
}
int blockSize;
if (elementName != null) {
blockSize = 1;
if (previousBlock != null && previousBlock.closeElementsOnBlockStart) {
endBlockEntry(previousBlock);
}
writer.writeStartElement(elementName);
switch (type) {
case INFORMATION:
writer.writeAttribute("type", "important"); //$NON-NLS-1$ //$NON-NLS-2$
break;
case NOTE:
writer.writeAttribute("type", "note"); //$NON-NLS-1$ //$NON-NLS-2$
break;
case WARNING:
writer.writeAttribute("type", "caution"); //$NON-NLS-1$ //$NON-NLS-2$
break;
case TIP:
writer.writeAttribute("type", "tip"); //$NON-NLS-1$ //$NON-NLS-2$
break;
case PANEL:
writer.writeAttribute("type", "other"); //$NON-NLS-1$ //$NON-NLS-2$
break;
}
applyAttributes(attributes);
if (elementNames != null) {
for (String name : elementNames) {
writer.writeStartElement(name);
}
}
if (allowTitle && attributes.getTitle() != null) {
if (phraseTitle) {
writer.writeStartElement("ph"); //$NON-NLS-1$
writer.writeAttribute("outputclass", "title"); //$NON-NLS-1$ //$NON-NLS-2$
writer.writeCharacters(attributes.getTitle());
writer.writeEndElement();
} else {
writer.writeStartElement("title"); //$NON-NLS-1$
writer.writeCharacters(attributes.getTitle());
writer.writeEndElement();
}
}
} else {
blockSize = 0;
}
blockDescriptions.push(new BlockDescription(type, blockSize, elementNames, closeElementsOnBlockStart));
}
@Override
public void endBlock() {
final BlockDescription blockDescription = blockDescriptions.pop();
int size = blockDescription.size + blockDescription.entrySize;
for (int x = 0; x < size; ++x) {
writer.writeEndElement();
}
}
private void endBlockEntry(BlockDescription blockDescription) {
for (int x = 0; x < blockDescription.entrySize; ++x) {
writer.writeEndElement();
}
blockDescription.entrySize = 0;
}
private void openBlockEntry(BlockDescription blockDescription, String[] entry) {
for (String ent : entry) {
writer.writeStartElement(ent);
}
blockDescription.entrySize += entry.length;
}
@Override
public void beginDocument() {
writer.writeStartDocument();
writer.writeDTD(doctype);
if (rootTitle != null) {
writer.writeStartElement("topic"); //$NON-NLS-1$
writer.writeStartElement("title"); //$NON-NLS-1$
writer.writeCharacters(rootTitle);
writer.writeEndElement();
}
}
@Override
public void beginHeading(int level, Attributes attributes) {
closeTopics(Math.max(level - 1, 0));
if (topicInfos.isEmpty() || topicInfos.peek().headingLevel < level) {
TopicInfo topicInfo = new TopicInfo();
topicInfo.headingLevel = level;
topicInfo.openElements = 2;
topicInfos.push(topicInfo);
writer.writeStartElement("topic"); //$NON-NLS-1$
if (attributes != null) {
applyAttributes(attributes);
attributes = null;
}
writer.writeStartElement("title"); //$NON-NLS-1$
}
}
private void applyAttributes(Attributes attributes) {
if (attributes.getId() != null) {
writer.writeAttribute("id", attributes.getId()); //$NON-NLS-1$
}
if (attributes.getCssClass() != null) {
writer.writeAttribute("outputclass", attributes.getCssClass()); //$NON-NLS-1$
}
}
@Override
public void beginSpan(SpanType type, Attributes attributes) {
ensureOpenTopic();
switch (type) {
case BOLD:
case STRONG:
writer.writeStartElement("b"); //$NON-NLS-1$
break;
case CITATION:
writer.writeStartElement("cite"); //$NON-NLS-1$
break;
case CODE:
writer.writeStartElement("codeph"); //$NON-NLS-1$
break;
case DELETED:
// no equivalent?
writer.writeStartElement("ph"); //$NON-NLS-1$
attributes
.setCssClass(attributes.getCssClass() == null ? "deleted" : attributes.getCssClass() + " deleted"); //$NON-NLS-1$ //$NON-NLS-2$
break;
case EMPHASIS:
writer.writeStartElement("i"); //$NON-NLS-1$
break;
case INSERTED:
// no equivalent?
writer.writeStartElement("ph"); //$NON-NLS-1$
attributes.setCssClass(attributes.getCssClass() == null
? "inserted" //$NON-NLS-1$
: attributes.getCssClass() + " inserted"); //$NON-NLS-1$
break;
case UNDERLINED:
writer.writeStartElement("u"); //$NON-NLS-1$
break;
case ITALIC:
case MARK:
writer.writeStartElement("i"); //$NON-NLS-1$
break;
case SPAN:
writer.writeStartElement("ph"); //$NON-NLS-1$
break;
case SUBSCRIPT:
writer.writeStartElement("sub"); //$NON-NLS-1$
break;
case SUPERSCRIPT:
writer.writeStartElement("sup"); //$NON-NLS-1$
break;
case MONOSPACE:
writer.writeStartElement("tt"); //$NON-NLS-1$
break;
case QUOTE:
writer.writeStartElement("q"); //$NON-NLS-1$
break;
case LINK: {
LinkAttributes linkAttributes = (LinkAttributes) attributes;
writer.writeStartElement("xref"); //$NON-NLS-1$
writer.writeAttribute("href", computeDitaXref(linkAttributes.getHref())); //$NON-NLS-1$
}
break;
default:
Logger.getLogger(DocBookDocumentBuilder.class.getName()).warning("No DITA topic mapping for " + type); //$NON-NLS-1$
writer.writeStartElement("ph"); //$NON-NLS-1$
break;
}
applyAttributes(attributes);
}
@Override
public void endSpan() {
writer.writeEndElement();
}
@Override
public void charactersUnescaped(String literal) {
ensureOpenTopic();
// note: this *may* have HTML tags in it
writer.writeLiteral(literal);
}
private void ensureOpenTopic() {
if (topicInfos.isEmpty()) {
beginHeading(1, new Attributes());
endHeading();
}
}
private void closeTopics(int toLevel) {
if (toLevel < 0) {
toLevel = 0;
}
while (!topicInfos.isEmpty() && topicInfos.peek().headingLevel > toLevel) {
TopicInfo topicInfo = topicInfos.pop();
for (int x = 0; x < topicInfo.openElements; ++x) {
writer.writeEndElement();
}
}
if (!topicInfos.isEmpty()) {
TopicInfo topicInfo = topicInfos.peek();
while (topicInfo.openElements > 1) {
--topicInfo.openElements;
writer.writeEndElement();
}
}
}
@Override
public void endDocument() {
closeTopics(0);
if (rootTitle != null) {
writer.writeEndElement();
}
writer.writeEndDocument();
}
@Override
public void endHeading() {
writer.writeEndElement(); // title
writer.writeStartElement("body"); //$NON-NLS-1$
}
@Override
public void image(Attributes attributes, String url) {
ensureOpenTopic();
boolean emitAsFigure = attributes.getTitle() != null;
if (emitAsFigure) {
writer.writeStartElement("fig"); //$NON-NLS-1$
writer.writeStartElement("title");//$NON-NLS-1$
writer.writeCharacters(attributes.getTitle());
writer.writeEndElement();
}
writer.writeEmptyElement("image"); //$NON-NLS-1$
writer.writeAttribute("href", url); //$NON-NLS-1$
applyImageAttributes(attributes);
if (emitAsFigure) {
writer.writeEndElement();
}
}
@Override
public void imageLink(Attributes linkAttributes, Attributes imageAttributes, String href, String imageUrl) {
ensureOpenTopic();
writer.writeStartElement("xref"); //$NON-NLS-1$
writer.writeAttribute("href", computeDitaXref(href)); //$NON-NLS-1$
writer.writeAttribute("format", "html"); //$NON-NLS-1$ //$NON-NLS-2$
image(imageAttributes, imageUrl);
writer.writeEndElement();
}
private void applyImageAttributes(Attributes imageAttributes) {
applyAttributes(imageAttributes);
if (imageAttributes instanceof ImageAttributes) {
ImageAttributes attributes = (ImageAttributes) imageAttributes;
if (attributes.getAlt() != null) {
writer.writeAttribute("alt", attributes.getAlt()); //$NON-NLS-1$
}
if (attributes.getHeight() > 0) {
writer.writeAttribute("height", Integer.toString(attributes.getHeight())); //$NON-NLS-1$
}
if (attributes.getWidth() > 0) {
writer.writeAttribute("width", Integer.toString(attributes.getWidth())); //$NON-NLS-1$
}
if (attributes.getAlign() != null) {
switch (attributes.getAlign()) {
case Left:
writer.writeAttribute("align", "center"); //$NON-NLS-1$ //$NON-NLS-2$
break;
case Right:
writer.writeAttribute("align", "right"); //$NON-NLS-1$ //$NON-NLS-2$
break;
case Center:
writer.writeAttribute("align", "center"); //$NON-NLS-1$ //$NON-NLS-2$
break;
}
writer.writeAttribute("placement", "break"); //$NON-NLS-1$ //$NON-NLS-2$
}
}
}
@Override
public void lineBreak() {
// no equivalent in DITA?
}
@Override
public void link(Attributes attributes, String hrefOrHashName, String text) {
ensureOpenTopic();
writer.writeStartElement("xref"); //$NON-NLS-1$
writer.writeAttribute("href", computeDitaXref(hrefOrHashName)); //$NON-NLS-1$
if (text != null) {
characters(text);
}
writer.writeEndElement();
}
@Override
public void characters(String text) {
ensureOpenTopic();
super.characters(text);
}
@Override
public void entityReference(String entity) {
ensureOpenTopic();
if (entity.startsWith("#")) { //$NON-NLS-1$
String numeric = entity.substring(1);
int base = 10;
if (numeric.startsWith("x")) { //$NON-NLS-1$
numeric = entity.substring(1);
base = 16;
}
int unicodeValue = Integer.parseInt(numeric, base);
if (entityReferenceToUnicode.contains(unicodeValue)) {
writer.writeCharacters("" + ((char) unicodeValue)); //$NON-NLS-1$
return;
}
}
writer.writeEntityRef(entity);
}
/**
* the outline if available, otherwise null {@link #setOutline(OutlineItem)}
*/
public OutlineItem getOutline() {
return outline;
}
/**
* Set the outline of the document being parsed if xref URLs are to be correctly computed. OASIS DITA has its own
* URL syntax for DITA-specific links, which need some translation at the time that we build the document.
*/
public void setOutline(OutlineItem outline) {
this.outline = outline;
}
public String getFilename() {
return filename;
}
public void setFilename(String filename) {
this.filename = filename;
}
/**
* According to the DITA documentation, DITA content URLs use special syntax. this method translates internal URLs
* correctly according to the DITA rules.
*
* @return the href adjusted, or the original href if the given URL appears to be to non-document content
*/
private String computeDitaXref(String href) {
if (href.startsWith("#") && topicBreakLevel < Integer.MAX_VALUE) { //$NON-NLS-1$
if (outline != null) {
OutlineItem item = outline.findItemById(href.substring(1));
if (item != null) {
OutlineItem topicItem = computeTopicFileItem(item);
String targetFilename = computeTargetFilename(topicItem);
String ref;
if (targetFilename.equals(filename)) {
ref = href;
} else {
ref = targetFilename + href;
}
return ref;
}
}
}
return href;
}
public static String computeName(String headingId, String topicFilenameSuffix) {
String name = headingId == null ? __TOPIC : headingId.replaceAll("[^a-zA-Z0-9_.-]", "-"); //$NON-NLS-1$ //$NON-NLS-2$
name = name + topicFilenameSuffix;
return name;
}
private String computeTargetFilename(OutlineItem item) {
String filenameSuffix = filename.substring(filename.lastIndexOf('.'));
return computeName(item.getLevel() == topicBreakLevel ? item.getId() : null, filenameSuffix);
}
private OutlineItem computeTopicFileItem(OutlineItem item) {
while (item.getLevel() > topicBreakLevel && item.getParent() != null
&& item.getParent().getLevel() > (topicBreakLevel - 1)) {
item = item.getParent();
}
return item;
}
/**
* the heading level at which topics are determined
*/
public int getTopicBreakLevel() {
return topicBreakLevel;
}
/**
* the heading level at which topics are determined
*/
public void setTopicBreakLevel(int topicBreakLevel) {
this.topicBreakLevel = topicBreakLevel;
}
/**
* The title of the root topic if there should be one. If specified, the topic file is created with a 'wrapper' root
* topic with the given title.
*/
public void setRootTopicTitle(String rootTitle) {
this.rootTitle = rootTitle;
}
/**
* The title of the root topic if there should be one. If specified, the topic file is created with a 'wrapper' root
* topic with the given title.
*/
public String getRootTopicTitle() {
return rootTitle;
}
}