/******************************************************************************* * 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.BufferedOutputStream; import java.io.Closeable; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.Writer; import java.util.Stack; import org.eclipse.mylyn.wikitext.parser.Attributes; import org.eclipse.mylyn.wikitext.parser.outline.OutlineItem; import org.eclipse.mylyn.wikitext.util.DefaultXmlStreamWriter; import org.eclipse.mylyn.wikitext.util.FormattingXMLStreamWriter; import org.eclipse.mylyn.wikitext.util.XmlStreamWriter; /** * a document builder that can produce OASIS DITA output in the form of a book map and multiple topic output files, one * for each level-1 heading. This document builder differs from others in that it implements {@link Closeable} and * therefore must be closed after use. Also this document builder produces multiple output files. * * @author David Green * @see DocBookDocumentBuilder * @see MarkupToDitaTask * @since 3.0 */ public class DitaBookMapDocumentBuilder extends AbstractXmlDocumentBuilder implements Closeable { private String bookTitle; private String doctype = "<!DOCTYPE bookmap PUBLIC \"-//OASIS//DTD DITA 1.1 BookMap//EN\" \"http://docs.oasis-open.org/dita/v1.1/OS/dtd/bookmap.dtd\">"; //$NON-NLS-1$ private String topicDoctype; private String topicFilenameSuffix = ".dita"; //$NON-NLS-1$ private String topicFolder; private File targetFile; private DitaTopicDocumentBuilder currentTopic; private String latestHeadingId; private File currentTopicFile; private final Stack<Integer> headingLevels = new Stack<Integer>(); private boolean mapEntryOpen; private String titleText; private Writer currentTopicOut; private OutlineItem outline; private int topicBreakLevel = 1; private boolean formattingDependencies = true; public DitaBookMapDocumentBuilder(Writer out) { super(out); } public DitaBookMapDocumentBuilder(XmlStreamWriter writer) { super(writer); } @Override protected XmlStreamWriter createXmlStreamWriter(Writer out) { XmlStreamWriter writer = super.createXmlStreamWriter(out); return new FormattingXMLStreamWriter(writer); } /** * the book title as it should appear in the bookmap */ public String getBookTitle() { return bookTitle; } /** * the book title as it should appear in the bookmap */ public void setBookTitle(String bookTitle) { this.bookTitle = bookTitle; } /** * the doctype to be used for topics, or null if the default is to be used * * @see #getDoctype() */ public String getTopicDoctype() { return topicDoctype; } /** * the doctype to be used for topics, or null if the default is to be used * * @see #setDoctype(String) */ public void setTopicDoctype(String topicDoctype) { this.topicDoctype = topicDoctype; } /** * the doctype to be used for the bookmap, or null if the default is to be used */ public String getDoctype() { return doctype; } /** * the doctype to be used for the bookmap, or null if the default is to be used */ public void setDoctype(String doctype) { this.doctype = doctype; } /** * the filename suffix to use when producing topics. Should include the leading dot '.', for example '.dita'. The * default value is <code>.dita</code>. */ public String getTopicFilenameSuffix() { return topicFilenameSuffix; } /** * the filename suffix to use when producing topics. Should include the leading dot '.', for example '.dita'. The * default value is <code>.dita</code>. */ public void setTopicFilenameSuffix(String topicFilenameSuffix) { this.topicFilenameSuffix = topicFilenameSuffix; } /** * the relative folder name of the folder in which topic files should be produced, or null if the files should be * created within the same folder as the bookmap. */ public String getTopicFolder() { return topicFolder; } /** * the relative folder name of the folder in which topic files should be produced, or null if the files should be * created within the same folder as the bookmap. */ public void setTopicFolder(String topicFolder) { this.topicFolder = topicFolder; } /** * the target output file of the bookmap. used to compute relative paths to topic files. */ public File getTargetFile() { return targetFile; } /** * the target output file of the bookmap. used to compute relative paths to topic files. */ public void setTargetFile(File targetFile) { this.targetFile = targetFile; } private DitaTopicDocumentBuilder getCurrentTopic() { if (currentTopic == null) { try { currentTopicFile = computeFile(latestHeadingId); currentTopicOut = new OutputStreamWriter( new BufferedOutputStream(new FileOutputStream(currentTopicFile)), "utf-8"); //$NON-NLS-1$ } catch (IOException e1) { throw new IllegalStateException(e1); } // create a DITA map entry String relativeTopic = currentTopicFile.getName(); if (topicFolder != null) { relativeTopic = topicFolder + '/' + relativeTopic; } writer.writeEmptyElement("chapter"); //$NON-NLS-1$ writer.writeAttribute("href", relativeTopic); //$NON-NLS-1$ titleText = ""; //$NON-NLS-1$ mapEntryOpen = true; currentTopic = new DitaTopicDocumentBuilder(new DefaultXmlStreamWriter(currentTopicOut), formattingDependencies); if (topicDoctype != null) { currentTopic.setDoctype(topicDoctype); } currentTopic.setTopicBreakLevel(topicBreakLevel); currentTopic.setOutline(outline); currentTopic.setFilename(currentTopicFile.getName()); currentTopic.beginDocument(); } return currentTopic; } private File computeFile(String headingId) { String name = DitaTopicDocumentBuilder.computeName(headingId, topicFilenameSuffix); File folder = targetFile.getParentFile(); if (topicFolder != null) { folder = new File(folder, topicFolder); } return new File(folder, name); } @Override public void acronym(String text, String definition) { getCurrentTopic().acronym(text, definition); } @Override public void beginBlock(BlockType type, Attributes attributes) { getCurrentTopic().beginBlock(type, attributes); } @Override public void beginHeading(int level, Attributes attributes) { headingLevels.push(level); if (level <= topicBreakLevel) { closeCurrentTopic(); latestHeadingId = attributes.getId(); } getCurrentTopic().beginHeading(level, attributes); } @Override public void beginSpan(SpanType type, Attributes attributes) { getCurrentTopic().beginSpan(type, attributes); } @Override public void characters(String text) { if (mapEntryOpen) { titleText += text; } getCurrentTopic().characters(text); } @Override public void charactersUnescaped(String literal) { getCurrentTopic().charactersUnescaped(literal); } @Override public void endBlock() { getCurrentTopic().endBlock(); } @Override public void beginDocument() { writer.writeStartDocument(); writer.writeDTD(doctype); writer.writeStartElement("bookmap"); //$NON-NLS-1$ writer.writeStartElement("title"); //$NON-NLS-1$ if (bookTitle != null) { writer.writeCharacters(bookTitle); } writer.writeEndElement(); } @Override public void endDocument() { closeCurrentTopic(); writer.writeEndElement(); writer.writeEndDocument(); } private void closeCurrentTopic() { if (currentTopic != null) { currentTopic.endDocument(); currentTopic = null; if (currentTopicOut != null) { try { currentTopicOut.close(); } catch (IOException e) { throw new IllegalStateException(e); } currentTopicOut = null; } } } @Override public void endHeading() { int level = headingLevels.pop(); if (level <= topicBreakLevel) { if (mapEntryOpen) { mapEntryOpen = false; writer.writeAttribute("navtitle", titleText); //$NON-NLS-1$ titleText = null; } } getCurrentTopic().endHeading(); } @Override public void endSpan() { getCurrentTopic().endSpan(); } @Override public void entityReference(String entity) { getCurrentTopic().entityReference(entity); } @Override public void image(Attributes attributes, String url) { getCurrentTopic().image(attributes, url); } @Override public void imageLink(Attributes linkAttributes, Attributes imageAttributes, String href, String imageUrl) { getCurrentTopic().imageLink(linkAttributes, imageAttributes, href, imageUrl); } @Override public void lineBreak() { getCurrentTopic().lineBreak(); } @Override public void link(Attributes attributes, String hrefOrHashName, String text) { getCurrentTopic().link(attributes, hrefOrHashName, text); } /** * users of this class must call close when done with it. */ public void close() throws IOException { if (currentTopicOut != null) { try { currentTopicOut.close(); } catch (IOException e) { throw new IllegalStateException(e); } currentTopicOut = null; } } /** * 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; } /** * 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; } /** * Indicate if dependencies should be formatted */ public boolean isFormattingDependencies() { return formattingDependencies; } /** * Indicate if dependencies should be formatted */ public void setFormattingDependencies(boolean formattingDependencies) { this.formattingDependencies = formattingDependencies; } }