| /******************************************************************************* |
| * 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; |
| } |
| } |