package org.freemarker.docgen; import static org.freemarker.docgen.DocBook5Constants.A_FILEREF; import static org.freemarker.docgen.DocBook5Constants.A_TARGETDOC; import static org.freemarker.docgen.DocBook5Constants.A_XLINK_HREF; import static org.freemarker.docgen.DocBook5Constants.DOCUMENT_STRUCTURE_ELEMENTS; import static org.freemarker.docgen.DocBook5Constants.E_APPENDIX; import static org.freemarker.docgen.DocBook5Constants.E_ARTICLE; import static org.freemarker.docgen.DocBook5Constants.E_BOOK; import static org.freemarker.docgen.DocBook5Constants.E_CHAPTER; import static org.freemarker.docgen.DocBook5Constants.E_FOOTNOTE; import static org.freemarker.docgen.DocBook5Constants.E_GLOSSARY; import static org.freemarker.docgen.DocBook5Constants.E_GLOSSENTRY; import static org.freemarker.docgen.DocBook5Constants.E_IMAGEDATA; import static org.freemarker.docgen.DocBook5Constants.E_INDEX; import static org.freemarker.docgen.DocBook5Constants.E_INDEXTERM; import static org.freemarker.docgen.DocBook5Constants.E_INFO; import static org.freemarker.docgen.DocBook5Constants.E_INFORMALTABLE; import static org.freemarker.docgen.DocBook5Constants.E_LINK; import static org.freemarker.docgen.DocBook5Constants.E_OLINK; import static org.freemarker.docgen.DocBook5Constants.E_PART; import static org.freemarker.docgen.DocBook5Constants.E_PREFACE; import static org.freemarker.docgen.DocBook5Constants.E_PRIMARY; import static org.freemarker.docgen.DocBook5Constants.E_SECONDARY; import static org.freemarker.docgen.DocBook5Constants.E_SECTION; import static org.freemarker.docgen.DocBook5Constants.E_SIMPLESECT; import static org.freemarker.docgen.DocBook5Constants.E_SUBTITLE; import static org.freemarker.docgen.DocBook5Constants.E_TABLE; import static org.freemarker.docgen.DocBook5Constants.E_TITLE; import static org.freemarker.docgen.DocBook5Constants.VISIBLE_TOPLEVEL_ELEMENTS; import static org.freemarker.docgen.DocBook5Constants.XMLNS_DOCBOOK5; import static org.freemarker.docgen.DocBook5Constants.XMLNS_XLINK; import java.io.BufferedWriter; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.Writer; import java.nio.charset.Charset; import java.text.Collator; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.SortedMap; import java.util.TimeZone; import java.util.TreeMap; import java.util.regex.Pattern; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; import freemarker.cache.ClassTemplateLoader; import freemarker.cache.FileTemplateLoader; import freemarker.cache.MultiTemplateLoader; import freemarker.cache.TemplateLoader; import freemarker.ext.dom.NodeModel; import freemarker.log.Logger; import freemarker.template.Configuration; import freemarker.template.SimpleHash; import freemarker.template.SimpleScalar; import freemarker.template.Template; import freemarker.template.TemplateException; import freemarker.template.TemplateMethodModelEx; import freemarker.template.TemplateModel; import freemarker.template.TemplateModelException; import freemarker.template.TemplateScalarModel; import freemarker.template.utility.ClassUtil; import freemarker.template.utility.DateUtil; import freemarker.template.utility.DateUtil.DateParseException; import freemarker.template.utility.StringUtil; /** * Generates complete HTML-format documentation from a DocBook 5 (XML) book. * * <p>Usage: First set the JavaBean properties, then call {@link #execute()}; * These must be set: * <ul> * <li>{@link #setSourceDirectory(File)} * <li>{@link #setDestinationDirectory(File)} * <li>{@link #setOffline(Boolean)}, unless the configuration file specifies this * </ul> * * <p>All files and directories in the source directory will be copied into the * destination directory as is (recursively), except these, which will be * ignored: * <ul> * <li>file or directory whose name starts with <tt>"docgen-"</tt> or * <tt>"docgen."</tt>, or whose name is <tt>"docgen"</tt>. * <li>file directly in the source directory (not in a sub-directory) * with <tt>xml</tt> file extension * <li>file or directory whose name looks like it's just backup, temporary, * SVN-related or CVS-related entry. * </ul> * * <p>The following files/directories are treated specially: * <ul> * <li><p><tt>book.xml</tt> or <tt>article.xml</tt> (<b>required</b>): the * DocBook XML that we want to transform. It may contains XInclude-s so * it doesn't have to store the whole book or article. * * <li><p><tt>docgen.cjson</tt> file (optional): * contains Docgen settings. It uses an extended JSON syntax; see more * <a href="#cjsonLanguage">later</a>. * * <p>Some notes about the settings in general: * <ul> * <li>Values that are of type "map" keep the order of entries as they were specified, * where it matters.</li> * <li>Values that describe link targets support not only usual URL-s, but also URL-s like * {@code id:example} that links to the XML element with the given {@code xml:id} attribute, * and {@code olink:example} that links to an URL specified in the {@code olinks} settings * (see that later).</li> * <li>All settings are optional, except where noted otherwise</li> * </ul> * * <p>The supported settings are: * <ul> * <li> * <p><tt>logo</tt> (map, required): The image used as the site logo. The map must be like: * <tt>logo: { href: "http://example.com/", src: example.png, alt: "Example" }</tt>. * <li> * <p><tt>tabs</tt> (map): Defines the tabs on the top of the page. * Each tab meant to represent an independently generated section of the web site. One of the tabs * corresponds to the content that we are generating now, and that tab must be associated to the <tt>""</tt> * target URL in this setting. Example:<tt><br> * {<br> * "Home": "" // Empty - We are here<br> * "Manual": "olink:manual"<br> * "Java API": "olink:api"<br> * }</tt> * <li><p><tt>secondaryTabs</tt> (object): An array of objects for the * secondary tabs, like:<tt><br> * {<br> * "Contribute": { class: "icon-heart", href: "id:contribute" }<br> * "Report a Bug": { ... }<br> * ...<br> * }</tt> * <li><p><tt>ignoredFiles</tt> (object): The list of file name globs (can use: {@code *}, {@code ?} and * {@code **}) of the files that won't be copyied over as static files. The are relative to the topmost * source directory. * <li><p><tt>socialLinks</tt> (object): An array of objects for the * social links, like:<tt><br> * {<br> * "GitHub": { class: "icon-github", href: "https://github.com/example" },<br> * "Twitter": { ... }<br> * ...<br> * }</tt> * <li><p><tt>searchKey</tt> (string): A Google custom search key. If not * present, the search box will not show. * <li><p><tt>footerSiteMap</tt> (object): Defines the list of links to * display in the footer as columns. Example:<tt><br> * {<br> * "Column Title 1": { "Row Title 1": "id:someTraget", "Row Title 2": "id:someTraget" }<br> * "Column Title 2": { ... }<br> * ...<br> * }</tt> * <li><p><tt>internalBookmarks</tt> (map): Specifies the first part * of the book-mark link list that appears in the navigation bar. * Associates labels with element ID-s (<tt>xml:id</tt> attribute * values). * <li><p><tt>externalBookmarks</tt> (map): Specifies the second part * of the book-mark link list. Associates labels with arbitrary * URL-s or paths. External bookmarks should * be used to link to resources outside the documentation generated * by this configuration. If the target resource is controlled by * you, and so you can use the same set of tabs there as here, * consider using <tt>tabs</tt> instead. * <li><p><tt>offline</tt> (boolean): * Specifies if the documentation will be generated for offline use. * If it was already specified via {@link #setOffline(Boolean)}, then * that has priority. If it wasn't specified via {@link #setOffline(Boolean)}, * then it's mandatory to set. * <li><p><tt>removeNodesWhenOnline</tt> (list of strings): * The list of element {@code xml:id}-s of the XML elements that will be removed * from the DocBook document if {@code offline} is {@code false}. This is just like * if have deleted these elements (with their nested content) from the XML file. * (This meant to be used to remove sections that are redundant when the book is the * part of a bigger site.) * <li><p><tt>seoMeta</tt> (map): * Let you customize the metadata used by search engines for the selected pages. The page is selected with * the map's key, which is either an {@code xml:id} or {@code "file:"} + output file name. The value of * the key value pair is yet another map, which can have these keys (all optional): {@code description} * to specify a short page summary, {@code title} to override the title of the Docbook element, and * {@code fullTitle} to override the whole page title (shich used to be generated from the book title plus * the current element's title). When you modify the title or full title, it will change the HTML * {@code head/title} content, and some other elements in the {@code head} element, but not the * title shown in the HTML body or in the Table of Contents. Example value:<tt><br> * {<br> * "file:index.html": {<br> * "fullTitle": "Example Home Page"<br> * "description": "Just an example."<br> * }<br> * "someElementId": {<br> * "title": "Getting Started with Example"<br> * }<br> * }</tt> * <li><p><tt>deployUrl</tt> (string): URL the page is deployed to (used * for canonical URL-s) * <li><p><tt>olinks</tt> (map): * Maps <tt>olink</tt> <tt>targetdoc</tt> attribute values to * actual URL-s. * * <li><p><tt>validation</tt> (map): * This is where you can configure the optional Docgen-specific * DocBook validation restrictions. Accepted map entries are: * <ul> * <li><tt>programlistingsRequireRole</tt> (boolean): * defaults to {@code false}. * <li><tt>programlistingsRequireLanguage</tt> (boolean): * defaults to {@code false}. * <li><tt>outputFilesCanUseAutoID</tt> (boolean): * defaults to {@code false}. * <li><tt>maximumProgramlistingWidth</tt> (int): defaults to * {@link Integer#MAX_VALUE}. The maximum number of characters * per line in <tt>programlisting</tt>-s. * </ul> * * <li><p><tt>contentDirectory</tt> (string): By default the Docgen * configuration files and the files that store the actual book * content (DocBook XML-s, images, etc.) are in the same directory, * the so called source directory. By setting this setting, the last * can be separated from the directory of the configuration files. * If it's not an absolute path then it will be interpreted * relatively to the source directory. * * <li><p><a name="setting_lowestFileElementRank"></a> * <tt>lowestFileElementRank</tt> (string): The lowest document * structure element "rank" for which an own output file will be * created. Note that possibly not all such elements are shown in a * given TOF (Table of Files) due to the <tt>maxTOFDisplayDepth</tt> * or <tt>maxMainTOFDisplayDepth</tt> setting. * * <p>"rank" symbolizes how big structural unit the element stands * for. The valid ranks are, from the lowest to the highest: * {@code simplesect}, {@code section3}, {@code section2}, * {@code section1}, {@code chapter}, {@code part}, * {@code book}. * If the name of an element is the same as one of the rank names * then that will be its rank. For <tt>section</tt>-s, the number in * the rank name tells how deeply the <tt>section</tt> is nested into * other <tt>section</tt>-s (1 means a <tt>section</tt> that is not * nested into any other <tt>section</tt>-s). For the other document * structure elements (e.g. <tt>appendix</tt>, <tt>preface</tt>, * etc.) the rank will be decided based on its surroundings. For * example, <tt>book/appendix</tt>, if it has <tt>part</tt> siblings, * will receive <tt>part</tt> rank, but if it has <tt>chapter</tt> * siblings, then it will only receive <tt>chapter</tt> rank. Again * the same kind of element, <tt>appendix</tt>, inside a * <tt>chapter</tt> will only receive <tt>section1</tt> rank. * It's good to know that nothing will receive <tt>chapter</tt> rank * unless it's directly under a <tt>part</tt> element (not just a * {@code part}-ranked element!) or <tt>book</tt> element. However, * if the root element of the document is <tt>article</tt>, that * will receive <tt>chapter</tt> rank. * * <p>Note that the content of some elements, like of * <tt>preface</tt>-s, is kept in a single file regardless of this * setting. * * <p>The default value is <tt>section1</tt>. * * <li><p><tt>lowestPageTOCElementRank</tt> (string): * The lowest document structure element "rank" for which a * "Page Contents" ToC entry will be created. * * <p>About "ranks" see <a href="#setting_lowestFileElementRank">the * <tt>lowestFileElementRank</tt> setting</a>. * * <p>The default value is <tt>section3</tt>. * <li><p><tt>maxTOFDisplayDepth</tt> (int): In a given TOF * (Table of Files) (because there can be multiple TOF-s, like there * can be a book-level TOF, and then there can be chapter-level * TOF-s), this is the nesting level until TOF entries are actually * displayed. * Depth level 0 is considered to by the level where the * file-element of the HTML page which contains the TOF is. * Defaults to {@link Integer#MAX_VALUE}. Must be at least {@code 1}. * * <li><p><tt>maxMainTOFDisplayDepth</tt> (int): Same as * <tt>maxTOFDisplayDepth</tt>, but only applies to the TOF on the * first (index) page. Defaults to the value of * <tt>maxTOFDisplayDepth</tt>. * * <li><p><tt>numberedSections</tt> (boolean): Specifies if * <tt>section</tt> element titles should be shown with numbering. * This will result in titles like "2 something" "2.1 Something" or * "2.1.3 Something", even "B.1 Something" (last is a * <tt>section</tt> under Appendix B). * * <p>Note that within some elements, like inside <tt>preface</tt>-s, * nothing has prefixes (labels) so this setting is ignored there. * * <li><p><tt>generateEclipseTOC</tt> (boolean): Sets whether an Eclipse * ToC XML is generated for the generated HTML-s. Defaults to * <tt>false</tt>. * * <li><p><tt>eclipse</tt> (map): * Stores the settings of the Eclipse-ToC-generation. * (Note that you still must turn that on with * <tt>generateEclipseTOC</tt>; the mere presence * of this setting will not do that.). * Accepted map entries are: * <ul> * <li><tt>link_to</tt> (string): The value of * <tt>toc.@link_to</tt> in the generated ToC file. If not * specified, there will not be any <tt>link_to</tt> attribute. * </ul> * * <li><p><tt>locale</tt> (string): The "nationality" used for * lexical shorting, number formatting and such things. * Defaults to <tt>"en_US"</tt>. * * <li><p><tt>timeZone</tt> (string): The time zone used for the * date/time shown. Defaults to <tt>"GMT"</tt>. * * <li><p><tt>disableJavaScript</tt> (boolean): Disallow JavaScript in * the generated pages. Defaults to <tt>false</tt> (i.e., JavaScript * is allowed). The pages are more functional with JavaScript, but * MSIE 6 and 7 (didn't tried 8) will show a security alert and block * JavaScript if the page is opened from the local file-system (i.e., * as <tt>file://...</tt> or <tt>C:\...</tt>, etc). So if the * generated content is often read locally and the target audience is * not IT-people (who know this thing very well, since even Javadoc * output does this), you better set this to <tt>true</tt>. Note that * even with JavaScript blocked by MSIE, the page will remain as * functional as if you were generating it with * <tt>disableJavaScript</tt> set to <tt>true</tt>, only the security * warning is annoying. * * <li><p><tt>onlineTrackerHTML</tt> (string): The path of a HTML file * whose content will be inserted before the <tt>body</tt> tag, unless * <tt>offline</tt> was set to <tt>true</tt>. This is typically used to * insert the Google Analytics <tt>script</tt> element. If this path is * relative, it's relative to the source directory. * * <li><p><tt>showXXELogo</tt> (boolean): Specifies if an * "Edited with XXE" logo should be shown on the generated pages. * Defaults to <tt>false</tt>. * * </ul> * * <li><p><tt>docgen-templates</tt> directory: * The templates here will have priority over the ones in the * {@code org.freemarker.docgen.templates} package. * This is mostly used for overriding <tt>customizations.ftlh</tt>; * that FTL is <tt>#import</tt>-ed at the beginning of all * template files, and searched first for the * <tt>#visit</tt>/<tt>#recurse</tt> calls. * </ul> * * * <p><b><font size="+1"><a name="cjsonLanguage"></a> * The CJSON language * </font></b></p> * * <p>It's JSON extended with some features that make it more convenient for * configuration files: * <ul> * <li>String literals whose value only contains letters (UNICODE), digits * (UNICODE) and characters {@code '.'}, {@code '_'}, {@code '$'}, * {@code '@'}, and {@code '-'}, but don't start with * characters 0-9 or is {@code true} or {@code false}, need not be * quoted. Thus instead of * <tt>{"name": "Big Joe", "color": "red"}</tt> you can just * write <tt>{name: "Big Joe", color: red}</tt>. (There are no * variable references in CJSON.) * <li>In key-value pairs the value defaults to {@code true}. Like, instead * of <tt>{showLogo: true}</tt> you can just write <tt>{showLogo}</tt>. * <li>You can omit the commas that otherwise would be at the end of the line. * <li>JavaScript comments are supported (<tt>/* ... *<!-- -->/</tt> and * <tt>// ...</tt>) * <li>If a file is expected to contain a map, like most configuration * files are, putting the whole thing between <tt>{</tt> and <tt>}</tt> is * optional. * <li>Maps remember the order in which the entries were specified in the * expression. The consumer of the configuration file will not utilize * this for most settings anyway, but for certain kind of settings it's * just more intuitive than getting the entries in a some random order. * <li>A comma may be used after the last item of a list or map. * <li>Supports FTL raw string literals (e.g. {@code r"C:\Windows\System32"}). * <li>Supports function calls (e.g. {@code f(1, 2)}), although it's up to the * consumer to resolve them; the CJSON language itself doesn't define any * functions. * </ul> * * <p>When CJSON is stored in a file, the file extension should be * <tt>cjson</tt> and UTF-8 charset should be is used. However, the charset can * be overridden with a initial * <tt>// charset: <i>charsetName</i></tt> comment [*]. * Initial BOM is silently ignored. * * <blockquote> * <p>* The comment is considered to be a charset override only if when it's * decoded with ISO-8859-1 it stands that: * <ul> * <li>Apart from white-space (and an initial BOM) it's the first thing in * the file. * <li>It's a <tt>//</tt> comment, not a <tt>/* ... *<!-- -->/</tt> comment. * <li>Ignoring white-space, the first word inside the comment is * <tt>charset</tt> or <tt>encoding</tt> (they are equivalent). That's * followed by optional whitespace, then a colon, then optional * whitespace again. Then a non-whitespace character (the first letter of * the charset name). At this point the comment already counts as a * charset override. Starting from there, until the end of the line or * of the file (whichever comes first) all kind of characters can occur, * and they will all belong to the charset name (which will be * interpreted after trimming surrounding whitespace). * </ul> * </blockquote> */ public final class Transform { // ------------------------------------------------------------------------- // Constants: static final String FILE_BOOK = "book.xml"; static final String FILE_ARTICLE = "article.xml"; static final String FILE_SETTINGS = "docgen.cjson"; /** Used for the Table of Contents file when a different node was marked to be the index.html. */ static final String FILE_TOC_HTML = "toc.html"; static final String FILE_DETAILED_TOC_HTML = "detailed-toc.html"; static final String FILE_INDEX_HTML = "index.html"; static final String FILE_SEARCH_RESULTS_HTML = "search-results.html"; static final String FILE_TOC_JSON_TEMPLATE = "toc-json.ftl"; static final String FILE_TOC_JSON_OUTPUT = "toc.js"; static final String FILE_ECLIPSE_TOC_TEMPLATE = "eclipse-toc.ftlx"; static final String FILE_ECLIPSE_TOC_OUTPUT = "eclipse-toc.xml"; static final String DIR_TEMPLATES = "docgen-templates"; static final String FILE_SITEMAP_XML_TEMPLATE = "sitemap.ftlx"; static final String FILE_SITEMAP_XML_OUTPUT = "sitemap.xml"; static final String SETTING_IGNORED_FILES = "ignoredFiles"; static final String SETTING_VALIDATION = "validation"; static final String SETTING_OFFLINE = "offline"; static final String SETTING_SIMPLE_NAVIGATION_MODE = "simpleNavigationMode"; static final String SETTING_DEPLOY_URL = "deployUrl"; static final String SETTING_ONLINE_TRACKER_HTML = "onlineTrackerHTML"; static final String SETTING_REMOVE_NODES_WHEN_ONLINE = "removeNodesWhenOnline"; static final String SETTING_INTERNAL_BOOKMARKS = "internalBookmarks"; static final String SETTING_EXTERNAL_BOOKMARKS = "externalBookmarks"; static final String SETTING_COPYRIGHT_HOLDER = "copyrightHolder"; static final String SETTING_COPYRIGHT_START_YEAR = "copyrightStartYear"; static final String SETTING_SEO_META = "seoMeta"; static final String SETTING_LOGO = "logo"; static final String SETTING_LOGO_KEY_SRC = "src"; static final String SETTING_LOGO_KEY_ALT = "alt"; static final String SETTING_LOGO_KEY_HREF = "href"; static final String SETTING_TABS = "tabs"; static final String SETTING_SECONDARY_TABS = "secondaryTabs"; static final String SETTING_SOCIAL_LINKS = "socialLinks"; static final String SETTING_FOOTER_SITEMAP = "footerSiteMap"; static final String SETTING_OLINKS = "olinks"; static final String SETTING_ECLIPSE = "eclipse"; static final String SETTING_SHOW_EDITORAL_NOTES = "showEditoralNotes"; static final String SETTING_GENERATE_ECLIPSE_TOC = "generateEclipseTOC"; static final String SETTING_SHOW_XXE_LOGO = "showXXELogo"; static final String SETTING_SEARCH_KEY = "searchKey"; static final String SETTING_DISABLE_JAVASCRIPT = "disableJavaScript"; static final String SETTING_TIME_ZONE = "timeZone"; static final String SETTING_LOCALE = "locale"; static final String SETTING_CONTENT_DIRECTORY = "contentDirectory"; static final String SETTING_LOWEST_PAGE_TOC_ELEMENT_RANK = "lowestPageTOCElementRank"; static final String SETTING_LOWEST_FILE_ELEMENT_RANK = "lowestFileElementRank"; static final String SETTING_MAX_TOF_DISPLAY_DEPTH = "maxTOFDisplayDepth"; static final String SETTING_MAX_MAIN_TOF_DISPLAY_DEPTH = "maxMainTOFDisplayDepth"; static final String SETTING_NUMBERED_SECTIONS = "numberedSections"; static final String SETTING_VALIDATION_PROGRAMLISTINGS_REQ_ROLE = "programlistingsRequireRole"; static final String SETTING_VALIDATION_PROGRAMLISTINGS_REQ_LANG = "programlistingsRequireLanguage"; static final String SETTING_VALIDATION_OUTPUT_FILES_CAN_USE_AUTOID = "outputFilesCanUseAutoID"; static final String SETTING_VALIDATION_MAXIMUM_PROGRAMLISTING_WIDTH = "maximumProgramlistingWidth"; static final String SETTING_ECLIPSE_LINK_TO = "link_to"; static final String SETTING_SEO_META_KEY_TITLE = "title"; static final String SETTING_SEO_META_KEY_FULL_TITLE = "fullTitle"; static final String SETTING_SEO_META_KEY_DESCRIPTION = "description"; static final Set<String> SETTING_SEO_META_KEYS; static { SETTING_SEO_META_KEYS = new LinkedHashSet<>(); SETTING_SEO_META_KEYS.add(SETTING_SEO_META_KEY_TITLE); SETTING_SEO_META_KEYS.add(SETTING_SEO_META_KEY_FULL_TITLE); SETTING_SEO_META_KEYS.add(SETTING_SEO_META_KEY_DESCRIPTION); } static final String COMMON_LINK_KEY_CLASS = "class"; static final String COMMON_LINK_KEY_HREF = "href"; static final Set<String> COMMON_LINK_KEYS; static { COMMON_LINK_KEYS = new LinkedHashSet<>(); COMMON_LINK_KEYS.add(COMMON_LINK_KEY_CLASS); COMMON_LINK_KEYS.add(COMMON_LINK_KEY_HREF); } private static final String VAR_OFFLINE = SETTING_OFFLINE; private static final String VAR_SIMPLE_NAVIGATION_MODE = SETTING_SIMPLE_NAVIGATION_MODE; private static final String VAR_DEPLOY_URL = SETTING_DEPLOY_URL; private static final String VAR_ONLINE_TRACKER_HTML = SETTING_ONLINE_TRACKER_HTML; private static final String VAR_SHOW_EDITORAL_NOTES = "showEditoralNotes"; private static final String VAR_TRANSFORM_START_TIME = "transformStartTime"; private static final String VAR_SHOW_XXE_LOGO = SETTING_SHOW_XXE_LOGO; private static final String VAR_SEARCH_KEY = SETTING_SEARCH_KEY; private static final String VAR_DISABLE_JAVASCRIPT = SETTING_DISABLE_JAVASCRIPT; private static final String VAR_ECLIPSE_LINK_TO = SETTING_ECLIPSE_LINK_TO; private static final String VAR_INTERNAL_BOOKMARDS = SETTING_INTERNAL_BOOKMARKS; private static final String VAR_EXTERNAL_BOOKMARDS = SETTING_EXTERNAL_BOOKMARKS; private static final String VAR_LOGO = SETTING_LOGO; private static final String VAR_COPYRIGHT_HOLDER = SETTING_COPYRIGHT_HOLDER; private static final String VAR_COPYRIGHT_START_YEAR = SETTING_COPYRIGHT_START_YEAR; private static final String VAR_SEO_META_TITLE_OVERRIDE = "seoMetaTitleOverride"; private static final String VAR_SEO_META_FULL_TITLE_OVERRIDE = "seoMetaFullTitleOverride"; private static final String VAR_SEO_META_DESCRIPTION = "seoMetaDescription"; private static final String VAR_TABS = SETTING_TABS; private static final String VAR_SECONDARY_TABS = SETTING_SECONDARY_TABS; private static final String VAR_SOCIAL_LINKS = SETTING_SOCIAL_LINKS; private static final String VAR_FOOTER_SITEMAP = SETTING_FOOTER_SITEMAP; private static final String VAR_OLINKS = SETTING_OLINKS; private static final String VAR_TOC_DISPLAY_DEPTH = SETTING_MAX_TOF_DISPLAY_DEPTH; private static final String VAR_NUMBERED_SECTIONS = SETTING_NUMBERED_SECTIONS; private static final String VAR_INDEX_ENTRIES = "indexEntries"; private static final String VAR_STARTS_WITH_TOP_LEVEL_CONTENT = "startsWithTopLevelContent"; private static final String VAR_PAGE_TYPE = "pageType"; private static final String VAR_ALTERNATIVE_TOC_LINK = "alternativeTOCLink"; private static final String VAR_ALTERNATIVE_TOC_LABEL = "alternativeTOCLabel"; private static final String VAR_PARENT_FILE_ELEMENT = "parentFileElement"; private static final String VAR_NEXT_FILE_ELEMENT = "nextFileElement"; private static final String VAR_PREVIOUS_FILE_ELEMENT = "previousFileElement"; private static final String VAR_ROOT_ELEMENT = "rootElement"; private static final String VAR_SHOW_NAVIGATION_BAR = "showNavigationBar"; private static final String VAR_SHOW_BREADCRUMB = "showBreadCrumb"; private static final String VAR_JSON_TOC_ROOT = "tocRoot"; private static final String PAGE_TYPE_DETAILED_TOC = "docgen:detailed_toc"; private static final String PAGE_TYPE_SEARCH_RESULTS = "docgen:search_results"; private static final String OLINK_SCHEMA_START = "olink:"; private static final String ID_SCHEMA_START = "id:"; private static final Charset UTF_8 = Charset.forName("UTF-8"); static final String SYSPROP_GENERATION_TIME = "docgen.generationTime"; // Docgen-specific XML attributes (added during DOM-tree postediting): /** * Marks an element for which a separate file is created; attached to * document structure elements, value is always {@code "true"}. */ private static final String A_DOCGEN_FILE_ELEMENT = "docgen_file_element"; /** * Marks an element for which a page ToC ("Page Contents") line is shown; * attached to document structure elements, it's value is always * {@code "true"}. */ private static final String A_DOCGEN_PAGE_TOC_ELEMENT = "docgen_page_toc_element"; /** * Marks and element that is shown in the <em>detailed</em> main ToC; * attached to document structure elements, it's value is always * {@code "true"}. */ private static final String A_DOCGEN_DETAILED_TOC_ELEMENT = "docgen_detailed_toc_element"; /** * The top-level document-structure element is marked with this; * it's value is always {@code "true"}. */ private static final String A_DOCGEN_ROOT_ELEMENT = "docgen_root_element"; /** * The numbering or letter or whatever that is shown before the tile, such * as "2.4" or "IV"; attached to document structure elements that use a * title prefix. */ private static final String A_DOCGEN_TITLE_PREFIX = "docgen_title_prefix"; /** * The integer ordinal of the document structure element within its own ToC * level, counting all kind of preceding document structure siblings; * attached to the document structure element. * * @see #A_DOCGEN_NUMBERING */ private static final String A_DOCGEN_UNITED_NUMBERING = "docgen_united_numbering"; /** * Describes how "big" a title should be; attached to the document structure * element (no to the title element). For the possible values see the * {@code AV_DOCGEN_TITLE_RANK_...} constants. For even more information see * {@link #preprocessDOM_addRanks(Document)}. */ private static final String A_DOCGEN_RANK = "docgen_rank"; /** An element for which it's not possible to create a link. */ private static final String A_DOCGEN_NOT_ADDRESSABLE = "docgen_not_addressable"; private static final String AV_INDEX_ROLE = "index.html"; /** * This is how automatically added id attribute values start. */ static final String AUTO_ID_PREFIX = "autoid_"; static final String DOCGEN_ID_PREFIX = "docgen_"; /** Elements for which an id attribute automatically added if missing */ private static final Set<String> GUARANTEED_ID_ELEMENTS; static { Set<String> idAttElems = new HashSet<String>(); for (String elemName : DOCUMENT_STRUCTURE_ELEMENTS) { idAttElems.add(elemName); } idAttElems.add(E_GLOSSARY); idAttElems.add(E_GLOSSENTRY); GUARANTEED_ID_ELEMENTS = Collections.unmodifiableSet(idAttElems); } /** * Elements whose children will go into a single output file regardless * of the element ranks, and whose children never use title prefixes * (labels). */ private static final Set<String> PREFACE_LIKE_ELEMENTS; static { Set<String> sinlgeFileElems = new HashSet<String>(); sinlgeFileElems.add(E_PREFACE); PREFACE_LIKE_ELEMENTS = Collections.unmodifiableSet(sinlgeFileElems); } private static final String XMLNS_DOCGEN = "http://freemarker.org/docgen"; private static final String E_SEARCHRESULTS = "searchresults"; private static final String SEARCH_RESULTS_PAGE_TITLE = "Search results"; private static final String SEARCH_RESULTS_ELEMENT_ID = "searchresults"; // ------------------------------------------------------------------------- // Settings: private File destDir; private File srcDir; private File contentDir; private List<Pattern> ignoredFilePathPatterns = new ArrayList<>(); private Boolean offline; private String deployUrl; private String onlineTrackerHTML; private Set<String> removeNodesWhenOnline; /** Element types for which a new output file is created */ private DocumentStructureRank lowestFileElemenRank = DocumentStructureRank.SECTION1; private DocumentStructureRank lowestPageTOCElemenRank = DocumentStructureRank.SECTION3; private int maxTOFDisplayDepth = Integer.MAX_VALUE; private int maxMainTOFDisplayDepth; // 0 indicates "not set"; private boolean numberedSections; private boolean generateEclipseTOC; private boolean simpleNavigationMode; private boolean showEditoralNotes; private boolean showXXELogo; private String searchKey; private boolean disableJavaScript; private boolean validate = true; private Locale locale = Locale.US; private TimeZone timeZone = TimeZone.getTimeZone("GMT"); private boolean printProgress; private LinkedHashMap<String, String> internalBookmarks = new LinkedHashMap<String, String>(); private LinkedHashMap<String, String> externalBookmarks = new LinkedHashMap<>(); private Map<String, Map<String, String>> footerSiteMap; private LinkedHashMap<String, String> tabs = new LinkedHashMap<>(); private Map<String, Map<String, String>> secondaryTabs; private Map<String, Map<String, String>> socialLinks; private HashMap<String, String> logo; private String copyrightHolder; private Integer copyrightStartYear; private Map<String, Map<String, String>> seoMeta; private DocgenValidationOptions validationOps = new DocgenValidationOptions(); // ------------------------------------------------------------------------- // Global transformation state: private boolean executed; private Map<String, String> olinks = new HashMap<String, String>(); private Map<String, List<NodeModel>> primaryIndexTermLookup; private Map<String, SortedMap<String, List<NodeModel>>> secondaryIndexTermLookup; private Map<String, Element> elementsById; private List<TOCNode> tocNodes; private List<String> indexEntries; private Configuration fmConfig; // ------------------------------------------------------------------------- // Output-file-specific state: private TOCNode currentFileTOCNode; // ------------------------------------------------------------------------- // Misc. fields: private DocgenLogger logger = new DocgenLogger() { public void info(String message) { if (printProgress) { System.out.println(message); } } public void warning(String message) { if (printProgress) { System.out.println("Warning:" + message); } } }; // ------------------------------------------------------------------------- // Methods: /** * Loads the source XML and generates the output in the destination * directory. Don't forget to set JavaBean properties first. * * @throws DocgenException If a docgen-specific error occurs * @throws IOException If a file or other resource is missing or otherwise * can't be read/written. * @throws SAXException If the XML is not well-formed and valid, or the * SAX XML parsing has other problems. */ public void execute() throws DocgenException, IOException, SAXException { if (executed) { throw new DocgenException( "This transformation was alrady executed; " + "use a new " + Transform.class.getName() + "."); } executed = true; // Check Java Bean properties: if (srcDir == null) { throw new DocgenException( "The source directory (the DocBook XML) wasn't specified."); } if (!srcDir.isDirectory()) { throw new IOException( "Source directory doesn't exist: " + srcDir.getAbsolutePath()); } if (destDir == null) { throw new DocgenException( "The destination directory wasn't specified."); } // Note: This directory will be created automatically if missing. // Load configuration file: File templatesDir = null; String eclipseLinkTo = null; File cfgFile = new File(srcDir, FILE_SETTINGS); if (cfgFile.exists()) { Map<String, Object> cfg; try { cfg = CJSONInterpreter.evalAsMap(cfgFile); } catch (CJSONInterpreter.EvaluationException e) { throw new DocgenException(e.getMessage(), e.getCause()); } for (Entry<String, Object> cfgEnt : cfg.entrySet()) { final String settingName = cfgEnt.getKey(); final Object settingValue = cfgEnt.getValue(); if (settingName.equals(SETTING_IGNORED_FILES)) { List<String> patterns = castSettingToStringList(cfgFile, settingName, settingValue); for (String pattern : patterns) { ignoredFilePathPatterns.add(FileUtil.globToRegexp(pattern)); } } else if (settingName.equals(SETTING_OLINKS)) { Map<String, Object> m = castSettingToMap( cfgFile, settingName, settingValue); for (Entry<String, Object> ent : m.entrySet()) { String name = ent.getKey(); String target = castSettingValueMapValueToString( cfgFile, settingName, ent.getValue()); olinks.put(name, target); } } else if (settingName.equals(SETTING_INTERNAL_BOOKMARKS)) { Map<String, Object> m = castSettingToMap( cfgFile, settingName, settingValue); for (Entry<String, Object> ent : m.entrySet()) { String name = ent.getKey(); String target = castSettingValueMapValueToString( cfgFile, settingName, ent.getValue()); internalBookmarks.put(name, target); } // Book-mark targets will be checked later, when the XML // document is already loaded. } else if (settingName.equals(SETTING_EXTERNAL_BOOKMARKS)) { Map<String, Object> m = castSettingToMap( cfgFile, settingName, settingValue); for (Entry<String, Object> ent : m.entrySet()) { String name = ent.getKey(); String target = castSettingValueMapValueToString( cfgFile, settingName, ent.getValue()); externalBookmarks.put(name, target); } } else if (settingName.equals(SETTING_LOGO)) { Map<String, Object> m = castSettingToMap( cfgFile, settingName, settingValue); logo = new HashMap<>(); for (Entry<String, Object> ent : m.entrySet()) { String k = ent.getKey(); String v = castSettingValueMapValueToString(cfgFile, settingName, ent.getValue()); if (!(k.equals(SETTING_LOGO_KEY_SRC) || k.equals(SETTING_LOGO_KEY_ALT) || k.equals(SETTING_LOGO_KEY_HREF))) { throw newCfgFileException(cfgFile, SETTING_LOGO, "Unknown logo option: " + k); } logo.put(k, v); } if (!logo.containsKey(SETTING_LOGO_KEY_SRC)) { throw newCfgFileException(cfgFile, SETTING_LOGO, "Missing logo option: " + SETTING_LOGO_KEY_SRC); } if (!logo.containsKey(SETTING_LOGO_KEY_ALT)) { throw newCfgFileException(cfgFile, SETTING_LOGO, "Missing logo option: " + SETTING_LOGO_KEY_ALT); } if (!logo.containsKey(SETTING_LOGO_KEY_HREF)) { throw newCfgFileException(cfgFile, SETTING_LOGO, "Missing logo option: " + SETTING_LOGO_KEY_HREF); } } else if (settingName.equals(SETTING_COPYRIGHT_HOLDER)) { copyrightHolder = castSettingToString(cfgFile, settingName, settingValue); } else if (settingName.equals(SETTING_COPYRIGHT_START_YEAR)) { copyrightStartYear = castSettingToInt(cfgFile, settingName, settingValue); } else if (settingName.equals(SETTING_SEO_META)) { Map<String, Object> m = castSettingToMap( cfgFile, settingName, settingValue); seoMeta = new LinkedHashMap<>(); for (Entry<String, Object> ent : m.entrySet()) { String k = ent.getKey(); Map<String, String> v = castSettingValueMapValueToMapOfStringString( cfgFile, settingName, ent.getValue(), null, SETTING_SEO_META_KEYS); seoMeta.put(k, v); } } else if (settingName.equals(SETTING_TABS)) { Map<String, Object> m = castSettingToMap( cfgFile, settingName, settingValue); for (Entry<String, Object> ent : m.entrySet()) { String k = ent.getKey(); String v = castSettingValueMapValueToString(cfgFile, settingName, ent.getValue()); tabs.put(k, v); } } else if (settingName.equals(SETTING_SECONDARY_TABS)) { Map<String, Object> m = castSettingToMap( cfgFile, settingName, settingValue); secondaryTabs = new LinkedHashMap<>(); for (Entry<String, Object> ent : m.entrySet()) { String k = ent.getKey(); Map<String, String> v = castSettingValueMapValueToMapOfStringString( cfgFile, settingName, ent.getValue(), COMMON_LINK_KEYS, null); secondaryTabs.put(k, v); } } else if (settingName.equals(SETTING_SOCIAL_LINKS)) { Map<String, Object> m = castSettingToMap( cfgFile, settingName, settingValue); socialLinks = new LinkedHashMap<>(); for (Entry<String, Object> ent : m.entrySet()) { String entName = ent.getKey(); Map<String, String> entValue = castSettingValueMapValueToMapOfStringString( cfgFile, settingName, ent.getValue(), COMMON_LINK_KEYS, null); socialLinks.put(entName, entValue); } } else if (settingName.equals(SETTING_FOOTER_SITEMAP)) { // TODO Check value in more details footerSiteMap = (Map) castSettingToMap( cfgFile, settingName, settingValue); }else if (settingName.equals(SETTING_VALIDATION)) { Map<String, Object> m = castSettingToMap( cfgFile, SETTING_VALIDATION, settingValue); for (Entry<String, Object> ent : m.entrySet()) { String name = ent.getKey(); if (name.equals( SETTING_VALIDATION_PROGRAMLISTINGS_REQ_ROLE)) { validationOps.setProgramlistingRequiresRole( caseSettingToBoolean( cfgFile, settingName + "." + name, ent.getValue())); } else if (name.equals( SETTING_VALIDATION_PROGRAMLISTINGS_REQ_LANG)) { validationOps.setProgramlistingRequiresLanguage( caseSettingToBoolean( cfgFile, settingName + "." + name, ent.getValue())); } else if (name.equals( SETTING_VALIDATION_OUTPUT_FILES_CAN_USE_AUTOID) ) { validationOps.setOutputFilesCanUseAutoID( caseSettingToBoolean( cfgFile, settingName + "." + name, ent.getValue())); } else if (name.equals( SETTING_VALIDATION_MAXIMUM_PROGRAMLISTING_WIDTH) ) { validationOps.setMaximumProgramlistingWidth( castSettingToInt( cfgFile, settingName + "." + name, ent.getValue())); } else { throw newCfgFileException( cfgFile, SETTING_VALIDATION, "Unknown validation option: " + name); } } } else if (settingName.equals(SETTING_OFFLINE)) { if (offline == null) { // Ignore if the caller has already set this offline = caseSettingToBoolean(cfgFile, settingName, settingValue); } } else if (settingName.equals(SETTING_SIMPLE_NAVIGATION_MODE)) { simpleNavigationMode = caseSettingToBoolean(cfgFile, settingName, settingValue); } else if (settingName.equals(SETTING_DEPLOY_URL)) { deployUrl = castSettingToString(cfgFile, settingName, settingValue); } else if (settingName.equals(SETTING_ONLINE_TRACKER_HTML)) { String onlineTrackerHtmlPath = castSettingToString(cfgFile, settingName, settingValue); File f = new File(getSourceDirectory(), onlineTrackerHtmlPath); if (!f.exists()) { throw newCfgFileException( cfgFile, SETTING_ONLINE_TRACKER_HTML, "File not found: " + f.toPath()); } onlineTrackerHTML = FileUtil.loadString(f, UTF_8); } else if (settingName.equals(SETTING_REMOVE_NODES_WHEN_ONLINE)) { removeNodesWhenOnline = Collections.unmodifiableSet(new HashSet<String>( castSettingToStringList(cfgFile, settingName, settingValue))); } else if (settingName.equals(SETTING_ECLIPSE)) { Map<String, Object> m = castSettingToMap( cfgFile, settingName, settingValue); for (Entry<String, Object> ent : m.entrySet()) { String name = ent.getKey(); if (name.equals(SETTING_ECLIPSE_LINK_TO)) { String value = castSettingToString( cfgFile, settingName + "." + name, ent.getValue()); eclipseLinkTo = value; } else { throw newCfgFileException( cfgFile, settingName, "Unknown Eclipse option: " + name); } } } else if (settingName.equals(SETTING_LOCALE)) { String s = castSettingToString( cfgFile, settingName, settingValue); locale = StringUtil.deduceLocale(s); } else if (settingName.equals(SETTING_TIME_ZONE)) { String s = castSettingToString( cfgFile, settingName, settingValue); timeZone = TimeZone.getTimeZone(s); } else if (settingName.equals(SETTING_GENERATE_ECLIPSE_TOC)) { generateEclipseTOC = caseSettingToBoolean( cfgFile, settingName, settingValue); } else if (settingName.equals(SETTING_SHOW_EDITORAL_NOTES)) { showEditoralNotes = caseSettingToBoolean( cfgFile, settingName, settingValue); } else if (settingName.equals(SETTING_SHOW_XXE_LOGO)) { showXXELogo = caseSettingToBoolean( cfgFile, settingName, settingValue); } else if (settingName.equals(SETTING_SEARCH_KEY)) { searchKey = castSettingToString( cfgFile, settingName, settingValue); }else if (settingName.equals(SETTING_DISABLE_JAVASCRIPT)) { disableJavaScript = caseSettingToBoolean( cfgFile, settingName, settingValue); } else if (settingName.equals(SETTING_CONTENT_DIRECTORY)) { String s = castSettingToString( cfgFile, settingName, settingValue); contentDir = new File(srcDir, s); if (!contentDir.isDirectory()) { throw newCfgFileException(cfgFile, settingName, "It's not an existing directory: " + contentDir.getAbsolutePath()); } } else if (settingName.equals(SETTING_LOWEST_FILE_ELEMENT_RANK) || settingName.equals( SETTING_LOWEST_PAGE_TOC_ELEMENT_RANK)) { DocumentStructureRank rank; String strRank = castSettingToString( cfgFile, settingName, settingValue); try { rank = DocumentStructureRank.valueOf( strRank.toUpperCase()); } catch (IllegalArgumentException e) { String msg; if (strRank.equalsIgnoreCase("article")) { msg = "\"article\" is not a rank, since articles " + "can have various ranks depending on their " + "context. (Hint: if the article is the " + "top-level element then it has \"chapter\" " + "rank.)"; } else { msg = "Unknown rank: " + strRank; } throw newCfgFileException(cfgFile, settingName, msg); } if (settingName.equals( SETTING_LOWEST_FILE_ELEMENT_RANK)) { lowestFileElemenRank = rank; } else if (settingName.equals( SETTING_LOWEST_PAGE_TOC_ELEMENT_RANK)) { lowestPageTOCElemenRank = rank; } else { throw new BugException("Unexpected setting name."); } } else if (settingName.equals(SETTING_MAX_TOF_DISPLAY_DEPTH)) { maxTOFDisplayDepth = castSettingToInt( cfgFile, settingName, settingValue); if (maxTOFDisplayDepth < 1) { throw newCfgFileException(cfgFile, settingName, "Value must be at least 1."); } } else if (settingName.equals( SETTING_MAX_MAIN_TOF_DISPLAY_DEPTH)) { maxMainTOFDisplayDepth = castSettingToInt( cfgFile, settingName, settingValue); if (maxTOFDisplayDepth < 1) { throw newCfgFileException(cfgFile, settingName, "Value must be at least 1."); } } else if (settingName.equals(SETTING_NUMBERED_SECTIONS)) { numberedSections = caseSettingToBoolean( cfgFile, settingName, settingValue); } else { throw newCfgFileException(cfgFile, "Unknown setting: \"" + settingName + "\". (Hint: See the list of available " + "settings in the Java API documentation of " + Transform.class.getName() + ". Also, note that " + "setting names are case-sensitive.)"); } } // for each cfg settings if (offline == null) { throw new DocgenException( "The \"" + SETTING_OFFLINE + "\" setting wasn't specified; it must be set to true or false"); } if (logo == null) { throw new DocgenException( "The \"" + SETTING_LOGO + "\" setting wasn't specified; it must be set currently, as the layout reserves space for it."); } if (copyrightHolder == null) { throw new DocgenException( "The \"" + SETTING_COPYRIGHT_HOLDER + "\" setting wasn't specified."); } if (copyrightStartYear == null) { throw new DocgenException( "The \"" + SETTING_COPYRIGHT_START_YEAR + "\" setting wasn't specified."); } } // Ensure proper rank relations: if (lowestPageTOCElemenRank.compareTo(lowestFileElemenRank) > 0) { lowestPageTOCElemenRank = lowestFileElemenRank; } // Ensure {@link #maxMainTOFDisplayDepth} is set: if (maxMainTOFDisplayDepth == 0) { maxMainTOFDisplayDepth = maxTOFDisplayDepth; } templatesDir = new File(srcDir, DIR_TEMPLATES); if (!templatesDir.exists()) { templatesDir = null; } if (contentDir == null) { contentDir = srcDir; } // Initialize state fields primaryIndexTermLookup = new HashMap<String, List<NodeModel>>(); secondaryIndexTermLookup = new HashMap<String, SortedMap<String, List<NodeModel>>>(); elementsById = new HashMap<String, Element>(); tocNodes = new ArrayList<TOCNode>(); indexEntries = new ArrayList<String>(); // Setup FreeMarker: try { Logger.selectLoggerLibrary(Logger.LIBRARY_NONE); } catch (ClassNotFoundException e) { throw new BugException(e); } fmConfig = new Configuration(Configuration.VERSION_2_3_24); TemplateLoader templateLoader = new ClassTemplateLoader( Transform.class, "templates"); if (templatesDir != null) { templateLoader = new MultiTemplateLoader( new TemplateLoader[] { new FileTemplateLoader(templatesDir), templateLoader }); } fmConfig.setTemplateLoader(templateLoader); fmConfig.setLocale(locale); fmConfig.setTimeZone(timeZone); fmConfig.setDefaultEncoding(UTF_8.name()); fmConfig.setOutputEncoding(UTF_8.name()); // Do the actual job: // - Load and validate the book XML final File docFile; { final File docFile1 = new File(contentDir, FILE_BOOK); if (docFile1.isFile()) { docFile = docFile1; } else { final File docFile2 = new File(contentDir, FILE_ARTICLE); if (docFile2.isFile()) { docFile = docFile2; } else { throw new DocgenException("The book file is missing: " + docFile1.getAbsolutePath() + " or " + docFile2.getAbsolutePath()); } } } Document doc = XMLUtil.loadDocBook5XML( docFile, validate, validationOps, logger); ignoredFilePathPatterns.add(FileUtil.globToRegexp(docFile.getName())); // - Post-edit and examine the DOM: preprocessDOM(doc); // Resolve Docgen URL schemes in setting values: if (tabs != null) { for (Entry<String, String> tabEnt : tabs.entrySet()) { tabEnt.setValue(resolveDocgenURL(SETTING_TABS, tabEnt.getValue())); } } if (secondaryTabs != null) { for (Map<String, String> tab : secondaryTabs.values()) { tab.put("href", resolveDocgenURL(SETTING_SECONDARY_TABS, tab.get("href"))); } } if (externalBookmarks != null) { for (Entry<String, String> bookmarkEnt : externalBookmarks.entrySet()) { bookmarkEnt.setValue(resolveDocgenURL(SETTING_EXTERNAL_BOOKMARKS, bookmarkEnt.getValue())); } } if (socialLinks != null) { for (Map<String, String> tab : socialLinks.values()) { tab.put("href", resolveDocgenURL(SETTING_SOCIAL_LINKS, tab.get("href"))); } } if (footerSiteMap != null) { for (Map<String, String> links : footerSiteMap.values()) { for (Map.Entry<String, String> link : links.entrySet()) { link.setValue(resolveDocgenURL(SETTING_FOOTER_SITEMAP, link.getValue())); } } } if (logo != null) { String logoHref = logo.get(SETTING_LOGO_KEY_HREF); if (logoHref != null) { logo.put(SETTING_LOGO_KEY_HREF, resolveDocgenURL(SETTING_LOGO, logoHref)); } } // - Create destination directory: if (!destDir.isDirectory() && !destDir.mkdirs()) { throw new IOException("Failed to create destination directory: " + destDir.getAbsolutePath()); } // - Check internal book-marks: for (Entry<String, String> ent : internalBookmarks.entrySet()) { String id = ent.getValue(); if (!elementsById.containsKey(id)) { throw newCfgFileException(cfgFile, SETTING_INTERNAL_BOOKMARKS, "No element with id \"" + id + "\" exists in the book."); } } // - Setup common data-model variables: try { // Settings: fmConfig.setSharedVariable( VAR_OFFLINE, offline); fmConfig.setSharedVariable( VAR_SIMPLE_NAVIGATION_MODE, simpleNavigationMode); fmConfig.setSharedVariable( VAR_DEPLOY_URL, deployUrl); fmConfig.setSharedVariable( VAR_ONLINE_TRACKER_HTML, onlineTrackerHTML); fmConfig.setSharedVariable( VAR_SHOW_EDITORAL_NOTES, showEditoralNotes); fmConfig.setSharedVariable( VAR_SHOW_XXE_LOGO, showXXELogo); fmConfig.setSharedVariable( VAR_SEARCH_KEY, searchKey); fmConfig.setSharedVariable( VAR_DISABLE_JAVASCRIPT, disableJavaScript); fmConfig.setSharedVariable( VAR_OLINKS, olinks); fmConfig.setSharedVariable( VAR_NUMBERED_SECTIONS, numberedSections); fmConfig.setSharedVariable( VAR_LOGO, logo); fmConfig.setSharedVariable( VAR_COPYRIGHT_HOLDER, copyrightHolder); fmConfig.setSharedVariable( VAR_COPYRIGHT_START_YEAR, copyrightStartYear); fmConfig.setSharedVariable( VAR_TABS, tabs); fmConfig.setSharedVariable( VAR_SECONDARY_TABS, secondaryTabs); fmConfig.setSharedVariable( VAR_SOCIAL_LINKS, socialLinks); fmConfig.setSharedVariable( VAR_FOOTER_SITEMAP, footerSiteMap); fmConfig.setSharedVariable( VAR_EXTERNAL_BOOKMARDS, externalBookmarks); fmConfig.setSharedVariable( VAR_INTERNAL_BOOKMARDS, internalBookmarks); fmConfig.setSharedVariable( VAR_ROOT_ELEMENT, doc.getDocumentElement()); // Calculated data: { Date generationTime; String generationTimeStr = System.getProperty(SYSPROP_GENERATION_TIME); if (generationTimeStr == null) { generationTime = new Date(); } else { try { generationTime = DateUtil.parseISO8601DateTime(generationTimeStr, DateUtil.UTC, new DateUtil.TrivialCalendarFieldsToDateConverter()); } catch (DateParseException e) { throw new DocgenException( "Malformed \"" + SYSPROP_GENERATION_TIME + "\" system property value: " + generationTimeStr, e); } } fmConfig.setSharedVariable(VAR_TRANSFORM_START_TIME, generationTime); } fmConfig.setSharedVariable( VAR_INDEX_ENTRIES, indexEntries); int tofCntLv1 = countTOFEntries(tocNodes.get(0), 1); int tofCntLv2 = countTOFEntries(tocNodes.get(0), 2); fmConfig.setSharedVariable( VAR_SHOW_NAVIGATION_BAR, tofCntLv1 != 0 || internalBookmarks.size() != 0 || externalBookmarks.size() != 0); fmConfig.setSharedVariable( VAR_SHOW_BREADCRUMB, tofCntLv1 != tofCntLv2); // Helper methods and directives: fmConfig.setSharedVariable( "NodeFromID", nodeFromID); fmConfig.setSharedVariable( "CreateLinkFromID", createLinkFromID); fmConfig.setSharedVariable( "primaryIndexTermLookup", primaryIndexTermLookup); fmConfig.setSharedVariable( "secondaryIndexTermLookup", secondaryIndexTermLookup); fmConfig.setSharedVariable( "CreateLinkFromNode", createLinkFromNode); } catch (TemplateModelException e) { throw new BugException(e); } // - Generate ToC JSON-s: { logger.info("Generating ToC JSON..."); Template template = fmConfig.getTemplate(FILE_TOC_JSON_TEMPLATE); try (Writer wr = new BufferedWriter( new OutputStreamWriter( new FileOutputStream( new File(destDir, FILE_TOC_JSON_OUTPUT)), UTF_8))) { try { SimpleHash dataModel = new SimpleHash(fmConfig.getObjectWrapper()); dataModel.put(VAR_JSON_TOC_ROOT, tocNodes.get(0)); template.process(dataModel, wr, null, NodeModel.wrap(doc)); } catch (TemplateException e) { throw new BugException("Failed to generate ToC JSON " + "(see cause exception).", e); } } } // - Generate Sitemap XML: { logger.info("Generating Sitemap XML..."); Template template = fmConfig.getTemplate(FILE_SITEMAP_XML_TEMPLATE); try (Writer wr = new BufferedWriter( new OutputStreamWriter( new FileOutputStream( new File(destDir, FILE_SITEMAP_XML_OUTPUT)), UTF_8))) { try { SimpleHash dataModel = new SimpleHash(fmConfig.getObjectWrapper()); dataModel.put(VAR_JSON_TOC_ROOT, tocNodes.get(0)); template.process(dataModel, wr, null, NodeModel.wrap(doc)); } catch (TemplateException e) { throw new BugException("Failed to generate Sitemap XML" + "(see cause exception).", e); } } } // - Generate the HTML-s: logger.info("Generating HTML files..."); int htmlFileCounter = 0; for (TOCNode tocNode : tocNodes) { if (tocNode.getOutputFileName() != null) { try { currentFileTOCNode = tocNode; try { // All output-file-specific processing comes here. htmlFileCounter += generateHTMLFile(); } finally { currentFileTOCNode = null; } } catch (freemarker.core.StopException e) { throw new DocgenException(e.getMessage()); } catch (TemplateException e) { throw new BugException(e); } } } if (!offline && searchKey != null) { try { generateSearchResultsHTMLFile(doc); htmlFileCounter++; } catch (freemarker.core.StopException e) { throw new DocgenException(e.getMessage()); } catch (TemplateException e) { throw new BugException(e); } } // - Copy the standard statics: logger.info("Copying common static files..."); copyCommonStatic("docgen.css"); copyCommonStatic("docgen.min.css"); copyCommonStatic("img/patterned-bg.png"); copyCommonStatic("fonts/icomoon.eot"); copyCommonStatic("fonts/icomoon.svg"); copyCommonStatic("fonts/icomoon.ttf"); copyCommonStatic("fonts/icomoon.woff"); for (int i = 1; i < 15; i++) { copyCommonStatic("img/callouts/" + i + ".gif"); } if (showXXELogo) { copyCommonStatic("img/xxe.png"); } if (!disableJavaScript) { copyCommonStatic("main.js"); copyCommonStatic("main.min.js"); } // - Copy the custom statics: logger.info("Copying custom static files..."); int bookSpecStaticFileCounter = FileUtil.copyDir(contentDir, destDir, ignoredFilePathPatterns); // - Eclipse ToC: if (generateEclipseTOC) { if (simpleNavigationMode) { throw new DocgenException("Eclipse ToC generation is untested/unsupported with simpleNavigationMode=true."); } logger.info("Generating Eclipse ToC..."); Template template = fmConfig.getTemplate(FILE_ECLIPSE_TOC_TEMPLATE); try (Writer wr = new BufferedWriter( new OutputStreamWriter( new FileOutputStream( new File(destDir, FILE_ECLIPSE_TOC_OUTPUT)), UTF_8))) { try { SimpleHash dataModel = new SimpleHash(fmConfig.getObjectWrapper()); if (eclipseLinkTo != null) { dataModel.put(VAR_ECLIPSE_LINK_TO, eclipseLinkTo); } template.process(dataModel, wr, null, NodeModel.wrap(doc)); } catch (TemplateException e) { throw new BugException("Failed to generate Eclipse ToC " + "(see cause exception).", e); } } } // - Report summary: logger.info( "Done: " + htmlFileCounter + " HTML-s + " + bookSpecStaticFileCounter + " custom statics + commons" + (generateEclipseTOC ? " + Eclipse ToC" : "")); } /** * Resolves the URL if it uses the {@code "olink:"} or {@code "id:"} schema, returns it as if otherwise. */ private String resolveDocgenURL(String settingName, String url) throws DocgenException { if (url.startsWith(OLINK_SCHEMA_START)) { String oLinkName = url.substring(OLINK_SCHEMA_START.length()); String resolvedOLink = olinks.get(oLinkName); if (resolvedOLink == null) { throw new DocgenException("Undefined olink used inside configuration setting " + StringUtil.jQuote(settingName) + ": " + StringUtil.jQuote(oLinkName)); } return resolvedOLink; } else if (url.startsWith(ID_SCHEMA_START)) { String id = url.substring(ID_SCHEMA_START.length()); try { return createLinkFromId(id); } catch (DocgenException e) { throw new DocgenException("Can't resolve id inside configuration setting " + StringUtil.jQuote(settingName) + ": " + StringUtil.jQuote(id), e); } } else { return url; } } private DocgenException newCfgFileException( File cfgFile, String settingName, String desc) { settingName = settingName.replace(".", "\" per \""); return newCfgFileException(cfgFile, "Wrong value for setting \"" + settingName + "\": " + desc); } private DocgenException newCfgFileException(File cfgFile, String desc) { return newCfgFileException(cfgFile, desc, (Throwable) null); } private DocgenException newCfgFileException(File cfgFile, String desc, Throwable cause) { StringBuilder sb = new StringBuilder(); sb.append("Wrong configuration"); if (cfgFile != null) { sb.append(" file \""); sb.append(cfgFile.getAbsolutePath()); sb.append("\""); } sb.append(": "); sb.append(desc); return new DocgenException(sb.toString(), cause); } @SuppressWarnings("unchecked") private Map<String, Object> castSettingToMap( File cfgFile, String settingName, Object settingValue) throws DocgenException { if (!(settingValue instanceof Map)) { throw newCfgFileException( cfgFile, settingName, "Should be a map (like {key1: value1, key2: value2}), but " + "it's a " + CJSONInterpreter.cjsonTypeOf(settingValue) + "."); } return (Map<String, Object>) settingValue; } @SuppressWarnings("unchecked") private List<String> castSettingToStringList( File cfgFile, String settingName, Object settingValue) throws DocgenException { if (!(settingValue instanceof List)) { throw newCfgFileException( cfgFile, settingName, "Should be a list (like [value1, value2, ... valueN]), but " + "it's a " + CJSONInterpreter.cjsonTypeOf(settingValue) + "."); } List<?> settingValueAsList = (List<?>) settingValue; for (int i = 0; i < settingValueAsList.size(); i++) { Object listItem = settingValueAsList.get(i); if (!(listItem instanceof String)) { throw newCfgFileException( cfgFile, settingName, "Should be a list of String-s (like [\"value1\", \"value2\", ... \"valueN\"]), but at index " + i +" (0-based) there's a " + CJSONInterpreter.cjsonTypeOf(listItem) + "."); } } return (List<String>) settingValue; } private String castSettingToString(File cfgFile, String settingName, Object settingValue) throws DocgenException { if (!(settingValue instanceof String)) { throw newCfgFileException( cfgFile, settingName, "Should be a string, but it's a " + CJSONInterpreter.cjsonTypeOf(settingValue) + "."); } return (String) settingValue; } private boolean caseSettingToBoolean(File cfgFile, String settingName, Object settingValue) throws DocgenException { if (!(settingValue instanceof Boolean)) { throw newCfgFileException( cfgFile, settingName, "Should be a boolean (i.e., true or false), but it's a " + CJSONInterpreter.cjsonTypeOf(settingValue) + "."); } return (Boolean) settingValue; } private int castSettingToInt(File cfgFile, String settingName, Object settingValue) throws DocgenException { if (!(settingValue instanceof Number)) { throw newCfgFileException( cfgFile, settingName, "Should be an number, but it's a " + CJSONInterpreter.cjsonTypeOf(settingValue) + "."); } if (!(settingValue instanceof Integer)) { throw newCfgFileException( cfgFile, settingName, "Should be an integer number (32 bits max), but it's: " + settingValue); } return ((Integer) settingValue).intValue(); } /* Unused at the moment @SuppressWarnings("unchecked") private List<String> castSettingToListOfStrings(File cfgFile, String settingName, Object settingValue) throws DocgenException { if (!(settingValue instanceof List)) { throw newCfgFileException( cfgFile, settingName, "Should be a list, but it's a " + CJSONInterpreter.cjsonTypeOf(settingValue) + "."); } List ls = (List) settingValue; for (Object i : ls) { if (!(i instanceof String)) { throw newCfgFileException( cfgFile, settingName, "Should be a list of strings, but one if the list items " + "is a " + CJSONInterpreter.cjsonTypeOf(i) + "."); } } return ls; } */ private String castSettingValueMapValueToString(File cfgFile, String settingName, Object mapEntryValue) throws DocgenException { if (!(mapEntryValue instanceof String)) { throw newCfgFileException(cfgFile, settingName, "The values in the key-value pairs of this map must be " + "strings, but some of them is a " + CJSONInterpreter.cjsonTypeOf(mapEntryValue) + "."); } return (String) mapEntryValue; } @SuppressWarnings("unchecked") private Map<String, String> castSettingValueMapValueToMapOfStringString(File cfgFile, String settingName, Object mapEntryValue, Set<String> requiredKeys, Set<String> optionalKeys) throws DocgenException { if (!(mapEntryValue instanceof Map)) { throw newCfgFileException(cfgFile, settingName, "The values in the key-value pairs of this map must be " + "Map-s, but some of them is a " + CJSONInterpreter.cjsonTypeOf(mapEntryValue) + "."); } if (requiredKeys == null) requiredKeys = Collections.emptySet(); if (optionalKeys == null) optionalKeys = Collections.emptySet(); Map<?, ?> mapEntryValueAsMap = (Map<?, ?>) mapEntryValue; for (Entry<?, ?> valueEnt : mapEntryValueAsMap.entrySet()) { Object key = valueEnt.getKey(); if (!(key instanceof String)) { throw newCfgFileException(cfgFile, settingName, "The values in the key-value pairs of this map must be " + "Map<String, String>-s, but some of the keys is a " + CJSONInterpreter.cjsonTypeOf(mapEntryValue) + "."); } if (!(valueEnt.getValue() instanceof String)) { throw newCfgFileException(cfgFile, settingName, "The values in the key-value pairs of this map must be " + "Map<String, String>-s, but some of the values is a " + CJSONInterpreter.cjsonTypeOf(valueEnt.getValue()) + "."); } if (!requiredKeys.contains(key) && !optionalKeys.contains(key)) { StringBuilder sb = new StringBuilder(); sb.append("Unsupported key: "); sb.append(StringUtil.jQuote(key)); sb.append(". Supported keys are: "); boolean isFirst = true; for (String supportedKey : requiredKeys) { if (!isFirst) { sb.append(", "); } else { isFirst = false; } sb.append(StringUtil.jQuote(supportedKey)); } for (String supportedKey : optionalKeys) { if (!isFirst) { sb.append(", "); } else { isFirst = false; } sb.append(StringUtil.jQuote(supportedKey)); } throw newCfgFileException(cfgFile, settingName, sb.toString()); } } for (String requiredKey : requiredKeys) { if (!mapEntryValueAsMap.containsKey(requiredKey)) { throw newCfgFileException(cfgFile, settingName, "Missing map key from nested Map: " + requiredKey); } } return (Map<String, String>) mapEntryValue; } private void copyCommonStatic(String path) throws IOException { FileUtil.copyResourceIntoFile( Transform.class, "statics", path, new File(destDir, "docgen-resources")); } /** * Adds attribute <tt>id</tt> to elements that are in * <code>idAttrElements</code>, but has no id attribute yet. * Adding id-s is useful to create more precise HTML cross-links later. */ private void preprocessDOM(Document doc) throws SAXException, DocgenException { NodeModel.simplify(doc); preprocessDOM_applyRemoveNodesWhenOnlineSetting(doc); preprocessDOM_addRanks(doc); preprocessDOM_misc(doc); preprocessDOM_buildTOC(doc); } private static final class PreprocessDOMMisc_GlobalState { private int lastId; /** Style silencer: notAUtiltiyClass() never used */ private PreprocessDOMMisc_GlobalState() { notAUtiltiyClass(); } /** CheckStyle silencer */ void notAUtiltiyClass() { // Nop } } private static final class PreprocessDOMMisc_ParentSectState { private int upperRomanNumber = 1; private int lowerRomanNumber = 1; private int arabicNumber = 1; private int upperLatinNumber = 1; private int unitedNumber = 1; /** Style silencer: notAUtiltiyClass() never used */ private PreprocessDOMMisc_ParentSectState() { notAUtiltiyClass(); } /** CheckStyle silencer */ void notAUtiltiyClass() { // Nop } } private void preprocessDOM_misc(Document doc) throws SAXException, DocgenException { preprocessDOM_misc_inner(doc, new PreprocessDOMMisc_GlobalState(), new PreprocessDOMMisc_ParentSectState()); indexEntries = new ArrayList<String>(primaryIndexTermLookup.keySet()); Collections.sort(indexEntries, Collator.getInstance(locale)); } private void preprocessDOM_misc_inner( Node node, PreprocessDOMMisc_GlobalState globalState, PreprocessDOMMisc_ParentSectState parentSectState) throws SAXException, DocgenException { if (node instanceof Element) { Element elem = (Element) node; // xml:id -> id: String id = XMLUtil.getAttribute(elem, "xml:id"); if (id != null) { if (id.startsWith(AUTO_ID_PREFIX)) { throw new DocgenException( XMLUtil.theSomethingElement(elem, true) + " uses a reserved xml:id, " + TextUtil.jQuote(id) + ". All ID-s starting with " + "\"" + AUTO_ID_PREFIX + "\" are reserved for " + "Docgen."); } if (id.startsWith(DOCGEN_ID_PREFIX)) { throw new DocgenException( XMLUtil.theSomethingElement(elem, true) + " uses a reserved xml:id, " + TextUtil.jQuote(id) + ". All ID-s starting with " + "\"" + DOCGEN_ID_PREFIX + "\" are reserved for " + "Docgen."); } elem.setAttribute("id", id); } final String elemName = node.getNodeName(); // Add auto id-s: if (id == null && GUARANTEED_ID_ELEMENTS.contains(elemName)) { globalState.lastId++; id = AUTO_ID_PREFIX + globalState.lastId; elem.setAttribute("id", id); } if (id != null) { elementsById.put(id, elem); } // Add default titles: if (elemName.equals(E_PREFACE) || elemName.equals(E_GLOSSARY) || elemName.equals(E_INDEX)) { ensureTitleExists( elem, Character.toUpperCase(elemName.charAt(0)) + elemName.substring(1)); // Simplify tables: } else if ( (elemName.equals(E_INFORMALTABLE) || elemName.equals(E_TABLE)) && elem.getNamespaceURI().equals(XMLNS_DOCBOOK5)) { TableSimplifier.simplify(elem); // Collect index terms: } else if (elemName.equals(E_INDEXTERM)) { addIndexTerm(node); } else if (elemName.equals(E_IMAGEDATA)) { String ref = XMLUtil.getAttribute(elem, A_FILEREF); String loRef = ref.toLowerCase(); if (!loRef.startsWith("http://") && !loRef.startsWith("https://") && !ref.startsWith("/")) { if (!new File(contentDir, ref.replace('/', File.separatorChar)).isFile()) { throw new DocgenException( XMLUtil.theSomethingElement(elem) + " refers " + "to a missing file: \"" + ref.replace("\"", """) + "\""); } } if (loRef.endsWith(".svg")) { String pngRef = ref.substring(0, ref.length() - 4) + ".png"; if (!new File(contentDir, pngRef.replace('/', File.separatorChar)).isFile()) { throw new DocgenException( XMLUtil.theSomethingElement(elem) + " refers to an SVG file for which the fallback PNG file is missing: \"" + pngRef.replace("\"", """) + "\""); } } } // Adding title prefixes to document structure elements: if (DOCUMENT_STRUCTURE_ELEMENTS.contains(elemName)) { final String prefix; if (elem.getParentNode() instanceof Document) { // The document element is never prefixed prefix = null; } else if (hasPrefaceLikeParent(elem)) { prefix = null; } else if (numberedSections && elemName.equals(E_SECTION)) { prefix = String.valueOf( parentSectState.arabicNumber++); } else if (elemName.equals(E_CHAPTER)) { prefix = String.valueOf( parentSectState.arabicNumber++); } else if (elemName.equals(E_PART)) { prefix = TextUtil.toUpperRomanNumber( parentSectState.upperRomanNumber++); } else if (elemName.equals(E_APPENDIX)) { prefix = TextUtil.toUpperLatinNumber( parentSectState.upperLatinNumber++); } else if (elemName.equals(E_ARTICLE)) { prefix = TextUtil.toLowerRomanNumber( parentSectState.lowerRomanNumber++); } else { prefix = null; } if (prefix != null) { final String fullPrefix; final Node parent = elem.getParentNode(); if (parent instanceof Element // Don't inherit prefix from "part" rank: && !parent.getLocalName().equals(E_PART) // Don't inherit prefix from "article": && !parent.getLocalName().equals(E_ARTICLE)) { String inhPrefix = XMLUtil.getAttribute( (Element) parent, A_DOCGEN_TITLE_PREFIX); if (inhPrefix != null) { if (inhPrefix.endsWith(".")) { fullPrefix = inhPrefix + prefix; } else { fullPrefix = inhPrefix + "." + prefix; } } else { fullPrefix = prefix; } } else { fullPrefix = prefix; } elem.setAttribute(A_DOCGEN_TITLE_PREFIX, fullPrefix); } // if prefix != null elem.setAttribute( A_DOCGEN_UNITED_NUMBERING, String.valueOf(parentSectState.unitedNumber++)); // We will be the parent document structure element of the soon // processed children: parentSectState = new PreprocessDOMMisc_ParentSectState(); } // if document structure element } // if element NodeList children = node.getChildNodes(); int ln = children.getLength(); for (int i = 0; i < ln; i++) { preprocessDOM_misc_inner( children.item(i), globalState, parentSectState); } } private void preprocessDOM_applyRemoveNodesWhenOnlineSetting(Document doc) throws DocgenException { if (offline || removeNodesWhenOnline == null || removeNodesWhenOnline.isEmpty()) return; HashSet<String> idsToRemoveLeft = new HashSet<String>(removeNodesWhenOnline); preprocessDOM_applyRemoveNodesWhenOnlineSetting_inner( doc.getDocumentElement(), idsToRemoveLeft); if (!idsToRemoveLeft.isEmpty()) { throw new DocgenException( "These xml:id-s, specified in the \"" + SETTING_REMOVE_NODES_WHEN_ONLINE + "\" configuration setting, wasn't found in the document: " + idsToRemoveLeft); } } private void preprocessDOM_applyRemoveNodesWhenOnlineSetting_inner(Element elem, Set<String> idsToRemoveLeft) { Node child = elem.getFirstChild(); while (child != null && !idsToRemoveLeft.isEmpty()) { Element childElemToBeRemoved = null; if (child instanceof Element) { Element childElem = (Element) child; String id = XMLUtil.getAttribute(childElem, "xml:id"); if (id != null && idsToRemoveLeft.remove(id)) { childElemToBeRemoved = childElem; } if (!idsToRemoveLeft.isEmpty()) { preprocessDOM_applyRemoveNodesWhenOnlineSetting_inner(childElem, idsToRemoveLeft); } } child = child.getNextSibling(); if (childElemToBeRemoved != null) { elem.removeChild(childElemToBeRemoved); } } } /** * Annotates the document structure nodes with so called ranks. * About ranks see: {@link #setting_lowestFileElementRank}. */ private void preprocessDOM_addRanks(Document doc) throws DocgenException { Element root = doc.getDocumentElement(); String rootName = root.getLocalName(); if (rootName.equals(E_BOOK)) { root.setAttribute( A_DOCGEN_RANK, DocumentStructureRank.BOOK.toString()); preprocessDOM_addRanks_underBookRank(root); } else if (rootName.equals(E_ARTICLE)) { root.setAttribute( A_DOCGEN_RANK, DocumentStructureRank.CHAPTER.toString()); preprocessDOM_addRanks_underChapterRankOrDeeper(root, 0); } else { throw new DocgenException("The \"" + rootName + "\" element is " + "unsupported as root element."); } } private void preprocessDOM_addRanks_underBookRank( Element root) throws DocgenException { // Find the common rank: DocumentStructureRank commonRank = null; for (Element child : XMLUtil.childrenElementsOf(root)) { String name = child.getLocalName(); if (name.equals(E_PART)) { if (commonRank != null && !commonRank.equals(DocumentStructureRank.PART)) { throw new DocgenException("Bad document structure: " + XMLUtil.theSomethingElement(child) + " is on the " + "same ToC level with a \"" + E_CHAPTER + "\" element."); } commonRank = DocumentStructureRank.PART; } else if (name.equals(E_CHAPTER)) { if (commonRank != null && !commonRank.equals(DocumentStructureRank.CHAPTER)) { throw new DocgenException("Bad document structure: " + XMLUtil.theSomethingElement(child) + " is on the " + "same ToC level with a \"" + E_PART + "\" element."); } commonRank = DocumentStructureRank.CHAPTER; } } if (commonRank == null) { commonRank = DocumentStructureRank.CHAPTER; } // Apply the common rank plus go deeper: for (Element child : XMLUtil.childrenElementsOf(root)) { if (DOCUMENT_STRUCTURE_ELEMENTS.contains(child.getLocalName())) { child.setAttribute( A_DOCGEN_RANK, commonRank.toString()); // Even if this node received part rank, its children will not // "feel like" being the children of a true part, unless its // indeed a part: if (child.getLocalName().equals(E_PART)) { preprocessDOM_addRanks_underTruePart(child); } else { preprocessDOM_addRanks_underChapterRankOrDeeper( child, 0); } } } } private void preprocessDOM_addRanks_underTruePart( Node parent) throws DocgenException { for (Element child : XMLUtil.childrenElementsOf(parent)) { if (DOCUMENT_STRUCTURE_ELEMENTS.contains(child.getLocalName())) { child.setAttribute( A_DOCGEN_RANK, DocumentStructureRank.CHAPTER.toString()); preprocessDOM_addRanks_underChapterRankOrDeeper(child, 0); } } } private void preprocessDOM_addRanks_underChapterRankOrDeeper( Element parent, int underSectionRank) throws DocgenException { for (Element child : XMLUtil.childrenElementsOf(parent)) { if (DOCUMENT_STRUCTURE_ELEMENTS.contains(child.getLocalName())) { if (child.getLocalName().equals(E_SIMPLESECT)) { child.setAttribute( A_DOCGEN_RANK, DocumentStructureRank.SIMPLESECT.toString()); // Note: simplesection-s are leafs in the ToC hierarchy. } else { if (underSectionRank + 1 > DocgenRestrictionsValidator .MAX_SECTION_NESTING_LEVEL) { throw new DocgenException("Too deep ToC nesting for " + XMLUtil.theSomethingElement(child) + ": rank bellow " + DocumentStructureRank.sectionToString( DocgenRestrictionsValidator .MAX_SECTION_NESTING_LEVEL)); } child.setAttribute( A_DOCGEN_RANK, DocumentStructureRank.sectionToString( underSectionRank + 1)); preprocessDOM_addRanks_underChapterRankOrDeeper( child, underSectionRank + 1); } } } } private void preprocessDOM_buildTOC(Document doc) throws DocgenException { preprocessDOM_buildTOC_inner(doc, 0, null); if (tocNodes.size() > 0) { preprocessDOM_buildTOC_checkEnsureHasIndexHhml(tocNodes); preprocessDOM_buildTOC_checkTOCTopology(tocNodes.get(0)); if (!tocNodes.get(0).isFileElement()) { throw new BugException( "The root ToC node must be a file-element."); } preprocessDOM_buildTOC_checkFileTopology(tocNodes.get(0)); if (simpleNavigationMode) { // Must do it at the end: We need the docgen_... XML attributes here, and we must be past the // TOC topology checks. for (TOCNode tocNode : tocNodes) { if (tocNode.isFileElement() && (tocNode.getParent() == null || !hasTopLevelContent(tocNode.getElement()))) { tocNode.setOutputFileName(null); tocNode.getElement().setAttribute(A_DOCGEN_NOT_ADDRESSABLE, "true"); } } } if (!validationOps.getOutputFilesCanUseAutoID()) { for (TOCNode tocNode : tocNodes) { String outputFileName = tocNode.getOutputFileName(); if (outputFileName != null && outputFileName.startsWith(AUTO_ID_PREFIX)) { throw new DocgenException(XMLUtil.theSomethingElement(tocNode.getElement(), true) + " has automatically generated ID that is not allowed as the ID " + "is used for generating a file name. (Related setting: \"" + SETTING_VALIDATION + "\" per \"" + SETTING_VALIDATION_OUTPUT_FILES_CAN_USE_AUTOID + "\")"); } } } } } private static final String COMMON_TOC_TOPOLOGY_ERROR_HINT = " (Hint: Review the \"" + SETTING_LOWEST_PAGE_TOC_ELEMENT_RANK + "\" setting. Maybe it's incompatible with the structure of " + "this document.)"; private void preprocessDOM_buildTOC_checkTOCTopology(TOCNode tocNode) throws DocgenException { // Check parent-child relation: TOCNode parent = tocNode.getParent(); if (parent != null && !parent.getElement().isSameNode( tocNode.getElement().getParentNode())) { throw new DocgenException( "Bad ToC-element topology: In the ToC " + parent.theSomethingElement() + " is the parent of " + tocNode.theSomethingElement() + ", yet they are not in parent-child relation in the XML " + "document (but maybe in grandparent-nephew relation or " + "like)." + COMMON_TOC_TOPOLOGY_ERROR_HINT); } // Check following-sibling relation: TOCNode next = tocNode.getNext(); Element relevantSibling = preprocessDOM_buildTOC_getSectionLikeSibling( tocNode.getElement(), true); if (next != null) { if (relevantSibling == null) { throw new DocgenException( "Bad ToC-element topology: In the ToC " + next.theSomethingElement() + " is the following sibling of " + tocNode.theSomethingElement() + ", yet they are not siblings in the XML document." + COMMON_TOC_TOPOLOGY_ERROR_HINT); } if (!relevantSibling.isSameNode(next.getElement())) { throw new DocgenException( "Bad ToC-element topology: In the ToC " + next.theSomethingElement() + " is the immediate following sibling of " + tocNode.theSomethingElement() + ", but in the XML document there is a \"" + relevantSibling.getLocalName() + "\" element between them, or they aren't siblings " + "at all." + COMMON_TOC_TOPOLOGY_ERROR_HINT); } } else { // next == null if (relevantSibling != null) { throw new DocgenException( "Bad ToC-element topology: In the ToC hierarchy " + tocNode.theSomethingElement() + "\" is a last-child, but in the XML document it has " + "a \"" + relevantSibling.getLocalName() + "\" " + "element as its following sibling." + COMMON_TOC_TOPOLOGY_ERROR_HINT); } } // Check preceding-sibling relation: TOCNode prev = tocNode.getPrevious(); relevantSibling = preprocessDOM_buildTOC_getSectionLikeSibling( tocNode.getElement(), false); if (prev == null && relevantSibling != null) { throw new DocgenException( "Bad ToC-element topology: In the ToC hierarchy " + tocNode.theSomethingElement() + " is a first-child, " + "but in the XML document it has a " + "\"" + relevantSibling.getLocalName() + "\" " + "element as its preceding sibling." + COMMON_TOC_TOPOLOGY_ERROR_HINT); } TOCNode child = tocNode.getFirstChild(); while (child != null) { preprocessDOM_buildTOC_checkTOCTopology(child); child = child.getNext(); } } private Element preprocessDOM_buildTOC_getSectionLikeSibling( Element elem, boolean next) { Node relevantSibling = elem; do { if (next) { relevantSibling = relevantSibling.getNextSibling(); } else { relevantSibling = relevantSibling.getPreviousSibling(); } } while (relevantSibling != null && !(relevantSibling instanceof Element && DOCUMENT_STRUCTURE_ELEMENTS.contains( relevantSibling.getLocalName()))); return (Element) relevantSibling; } private static final String COMMON_FILE_TOPOLOGY_ERROR_HINT = " (Hint: Review the \"" + SETTING_LOWEST_FILE_ELEMENT_RANK + "\" setting. Maybe it's incompatible with the structure of " + "this document.)"; private void preprocessDOM_buildTOC_checkFileTopology(TOCNode tocNode) throws DocgenException { TOCNode firstChild = tocNode.getFirstChild(); if (firstChild != null) { boolean firstIsFileElement = firstChild.isFileElement(); TOCNode child = firstChild; do { if (child.isFileElement() != firstIsFileElement) { throw new DocgenException("Bad file-element topology: " + "The first child element of " + tocNode.theSomethingElement() + ", " + firstChild.theSomethingElement() + ", is " + (firstIsFileElement ? "a" : "not a") + " file-element, while another child, " + child.theSomethingElement() + (firstIsFileElement ? " isn't" : " is") + ". Either all relevant children elements must be " + "file-elements or neither can be." + COMMON_FILE_TOPOLOGY_ERROR_HINT); } preprocessDOM_buildTOC_checkFileTopology(child); child = child.getNext(); } while (child != null); if (firstIsFileElement && !tocNode.isFileElement()) { throw new DocgenException("Bad file-element topology: " + tocNode.theSomethingElement() + " is not a " + "file-element, yet it has file-element children, " + firstChild.theSomethingElement() + ". Only " + "file-elements can have children that are " + "file-elements."); } } } private TOCNode preprocessDOM_buildTOC_inner(Node node, final int sectionLevel, TOCNode parentTOCNode) throws DocgenException { TOCNode curTOCNode = null; int newSectionLevel = sectionLevel; if (node instanceof Element) { final Element elem = (Element) node; final String nodeName = node.getNodeName(); if (DOCUMENT_STRUCTURE_ELEMENTS.contains(nodeName)) { DocumentStructureRank rank = DocumentStructureRank.valueOf( XMLUtil.getAttribute(elem, A_DOCGEN_RANK) .toUpperCase()); final boolean isTheDocumentElement = elem.getParentNode() instanceof Document; if (isTheDocumentElement || rank.compareTo(lowestPageTOCElemenRank) >= 0) { curTOCNode = new TOCNode(elem, tocNodes.size()); tocNodes.add(curTOCNode); if ((isTheDocumentElement || rank.compareTo(lowestFileElemenRank) >= 0) && !hasPrefaceLikeParent(elem)) { elem.setAttribute(A_DOCGEN_FILE_ELEMENT, "true"); curTOCNode.setFileElement(true); if (isTheDocumentElement) { curTOCNode.setOutputFileName(FILE_TOC_HTML); elem.setAttribute(A_DOCGEN_ROOT_ELEMENT, "true"); } else if (getExternalLinkTOCNodeURLOrNull(elem) != null) { curTOCNode.setOutputFileName(null); } else if (AV_INDEX_ROLE.equals(elem.getAttribute(DocBook5Constants.A_ROLE))) { curTOCNode.setOutputFileName(FILE_INDEX_HTML); } else { String id = XMLUtil.getAttribute(elem, "id"); if (id == null) { throw new BugException("Missing id attribute"); } String fileName = id + ".html"; if (fileName.equals(FILE_TOC_HTML) || fileName.equals(FILE_DETAILED_TOC_HTML) || fileName.equals(FILE_INDEX_HTML) || fileName.equals(FILE_SEARCH_RESULTS_HTML)) { throw new DocgenException( XMLUtil.theSomethingElement(elem, true) + " has an xml:id that is deduced to " + "a reserved output file name, \"" + fileName + "\". (Hint: Change the " + "xml:id.)"); } curTOCNode.setOutputFileName(fileName); } } else { // of: if file element elem.setAttribute(A_DOCGEN_PAGE_TOC_ELEMENT, "true"); } elem.setAttribute(A_DOCGEN_DETAILED_TOC_ELEMENT, "true"); } // if ToC element } // if document structure element } // if Element if (curTOCNode != null) { parentTOCNode = curTOCNode; } NodeList children = node.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { TOCNode child = preprocessDOM_buildTOC_inner( children.item(i), newSectionLevel, parentTOCNode); if (child != null && parentTOCNode != null) { child.setParent(parentTOCNode); TOCNode lastChild = parentTOCNode.getLastChild(); if (lastChild != null) { child.setPrevious(lastChild); lastChild.setNext(child); } if (parentTOCNode.getFirstChild() == null) { parentTOCNode.setFirstChild(child); } parentTOCNode.setLastChild(child); } } return curTOCNode; } private String getExternalLinkTOCNodeURLOrNull(Element elem) throws DocgenException { if (elem.getParentNode() instanceof Document) { // The document element is never an external link ToC node. return null; } Element title = getTitle(elem); if (title == null) { // An element without title can't be an external link ToC node return null; } Iterator<Element> it = XMLUtil.childrenElementsOf(title).iterator(); if (it.hasNext()) { Element firstChild = it.next(); if (!it.hasNext()) { // It's the only child String firstChildName = firstChild.getLocalName(); if (firstChildName.equals(E_LINK)) { String href = XMLUtil.getAttributeNS(firstChild, XMLNS_XLINK, A_XLINK_HREF); if (href == null) { throw new DocgenException(XMLUtil.theSomethingElement(firstChild, true) + " inside a title has no xlink:" + A_XLINK_HREF + " attribute, thus it can't be " + "used as ToC link."); } return href; } else if (firstChildName.equals(E_OLINK)) { String targetdoc = XMLUtil.getAttributeNS(firstChild, null, A_TARGETDOC); if (targetdoc == null) { throw new DocgenException(XMLUtil.theSomethingElement(firstChild, true) + " has no xlink:" + A_TARGETDOC + " attribute"); } String url = olinks.get(targetdoc); if (url == null) { throw new DocgenException(XMLUtil.theSomethingElement(firstChild, true) + " refers to undefined olink name " + StringUtil.jQuote(targetdoc) + "; check configuration."); } return url; } } } return null; } /** * Ensures that * @param tocNodes * @throws DocgenException */ private void preprocessDOM_buildTOC_checkEnsureHasIndexHhml(List<TOCNode> tocNodes) throws DocgenException { for (TOCNode tocNode : tocNodes) { if (tocNode.getOutputFileName() != null && tocNode.getOutputFileName().equals(FILE_INDEX_HTML)) { return; } } // If we had no index.html, the ToC HTML will be renamed to it: for (TOCNode tocNode : tocNodes) { if (tocNode.getOutputFileName() != null && tocNode.getOutputFileName().equals(FILE_TOC_HTML)) { tocNode.setOutputFileName(FILE_INDEX_HTML); return; } } throw new DocgenException( "No " + FILE_INDEX_HTML + " output file would be generated. Add " + DocBook5Constants.A_ROLE + "=\"" + AV_INDEX_ROLE + "\" to one of the elements for which a separate file is generated."); } private boolean hasPrefaceLikeParent(Element elem) { while (true) { Node parent = elem.getParentNode(); if (parent != null && parent instanceof Element) { elem = (Element) parent; if (elem.getNamespaceURI().equals(XMLNS_DOCBOOK5) && PREFACE_LIKE_ELEMENTS.contains( elem.getLocalName())) { return true; } } else { return false; } } } private Element getTitle(Element elem) { NodeList children = elem.getChildNodes(); int ln = children.getLength(); for (int i = 0; i < ln; i++) { Node child = children.item(i); if (child instanceof Element && child.getLocalName().equals("title")) { return (Element) child; // !! found it } } return null; } private void ensureTitleExists(Element elem, String defaultTitle) { if (getTitle(elem) != null) { return; } // Retrieve a document node: Node node = elem; do { node = node.getParentNode(); if (node == null) { throw new BugException("Can't find Document node."); } } while (node.getNodeType() != Node.DOCUMENT_NODE); Document doc = (Document) node; // Create the title node: Element title = doc.createElementNS(XMLNS_DOCBOOK5, E_TITLE); title.appendChild(doc.createTextNode(defaultTitle)); // Insert it into the tree: elem.insertBefore(title, elem.getFirstChild()); } /** * Returns the {@link TOCNode} that corresponds to the element, or * {@link null} if it's not a file element. Can be called only * after {@link #createLookupTables(Node, LookupCreatingState)}. */ private TOCNode getFileTOCNodeFor(Element elem) { for (TOCNode tocNode : tocNodes) { if (tocNode.isFileElement() && tocNode.getElement().isSameNode(elem)) { return tocNode; } } return null; } private void addIndexTerm(Node node) { Node primary = null; Node secondary = null; NodeList children = node.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { Node child = node.getChildNodes().item(i); if (child.getNodeType() == Node.ELEMENT_NODE) { if (child.getNodeName().equals(E_PRIMARY)) { primary = child; } else if (child.getNodeName().equals(E_SECONDARY)) { secondary = child; } } } String primaryText = primary.getFirstChild().getNodeValue().trim(); if (!primaryIndexTermLookup.containsKey(primaryText)) { primaryIndexTermLookup.put(primaryText, new ArrayList<NodeModel>()); } if (secondary != null) { if (!secondaryIndexTermLookup.containsKey(primaryText)) { secondaryIndexTermLookup.put( primaryText, new TreeMap<String, List<NodeModel>>()); } Map<String, List<NodeModel>> m = secondaryIndexTermLookup.get( primaryText); String secondaryText = secondary.getFirstChild().getNodeValue() .trim(); List<NodeModel> nodes = m.get(secondaryText); if (nodes == null) { nodes = new ArrayList<NodeModel>(); m.put(secondaryText, nodes); } nodes.add(NodeModel.wrap(node)); } else { primaryIndexTermLookup.get(primaryText).add(NodeModel.wrap(node)); } } /** * Generates a HTML file for the {@link #currentFileTOCNode}, maybe with * some accompanying HTML-s. */ private int generateHTMLFile() throws IOException, TemplateException { SimpleHash dataModel = new SimpleHash(fmConfig.getObjectWrapper()); TOCNode otherTOCNode; otherTOCNode = currentFileTOCNode; do { otherTOCNode = otherTOCNode.getPreviousInTraversarOrder(); } while (!(otherTOCNode == null || otherTOCNode.isFileElement())); dataModel.put( VAR_PREVIOUS_FILE_ELEMENT, otherTOCNode != null ? otherTOCNode.getElement() : null); otherTOCNode = currentFileTOCNode; do { otherTOCNode = otherTOCNode.getNextInTraversarOrder(); } while (!(otherTOCNode == null || otherTOCNode.isFileElement())); dataModel.put( VAR_NEXT_FILE_ELEMENT, otherTOCNode != null ? otherTOCNode.getElement() : null); otherTOCNode = currentFileTOCNode.getParent(); dataModel.put( VAR_PARENT_FILE_ELEMENT, otherTOCNode != null ? otherTOCNode.getElement() : null); Element curElem = currentFileTOCNode.getElement(); final boolean isTheDocumentElement = curElem.getParentNode() instanceof Document; dataModel.put( VAR_TOC_DISPLAY_DEPTH, isTheDocumentElement ? maxTOFDisplayDepth : maxMainTOFDisplayDepth); dataModel.put( VAR_STARTS_WITH_TOP_LEVEL_CONTENT, hasTopLevelContent(currentFileTOCNode.getElement())); if (seoMeta != null) { Map<String, String> seoMetaMap = seoMeta.get("file:" + currentFileTOCNode.getOutputFileName()); if (seoMetaMap == null) { String id = XMLUtil.getAttribute(currentFileTOCNode.getElement(), "id"); if (id != null) { seoMetaMap = seoMeta.get(id); } } if (seoMetaMap != null) { dataModel.put( VAR_SEO_META_TITLE_OVERRIDE, seoMetaMap.get(SETTING_SEO_META_KEY_TITLE)); dataModel.put( VAR_SEO_META_FULL_TITLE_OVERRIDE, seoMetaMap.get(SETTING_SEO_META_KEY_FULL_TITLE)); dataModel.put( VAR_SEO_META_DESCRIPTION, seoMetaMap.get(SETTING_SEO_META_KEY_DESCRIPTION)); } } boolean generateDetailedTOC = false; if (isTheDocumentElement) { // Find out if a detailed ToC will be useful: int mainTOFEntryCount = countTOFEntries( currentFileTOCNode, maxMainTOFDisplayDepth); if (mainTOFEntryCount != 0 // means, not a single-page output && mainTOFEntryCount < tocNodes.size() * 0.75) { generateDetailedTOC = true; dataModel.put( VAR_ALTERNATIVE_TOC_LINK, FILE_DETAILED_TOC_HTML); dataModel.put( VAR_ALTERNATIVE_TOC_LABEL, "show detailed"); } } generateHTMLFile_inner(dataModel, currentFileTOCNode.getOutputFileName()); if (generateDetailedTOC) { dataModel.put(VAR_PAGE_TYPE, PAGE_TYPE_DETAILED_TOC); dataModel.put( VAR_ALTERNATIVE_TOC_LINK, currentFileTOCNode.getOutputFileName()); dataModel.put( VAR_ALTERNATIVE_TOC_LABEL, "show simplified"); generateHTMLFile_inner(dataModel, FILE_DETAILED_TOC_HTML); return 2; } else { return 1; } } private void generateSearchResultsHTMLFile(Document doc) throws TemplateException, IOException, DocgenException { SimpleHash dataModel = new SimpleHash(fmConfig.getObjectWrapper()); dataModel.put(VAR_PAGE_TYPE, PAGE_TYPE_SEARCH_RESULTS); dataModel.put(VAR_TOC_DISPLAY_DEPTH, maxMainTOFDisplayDepth); // Create docgen:searchresults element that's no really in the XML file: Element searchresultsElem = doc.createElementNS(XMLNS_DOCGEN, E_SEARCHRESULTS); { // Docgen templates may expect page-elements to have an id: if (elementsById.containsKey(SEARCH_RESULTS_ELEMENT_ID)) { throw new DocgenException("Reserved element id \"" + SEARCH_RESULTS_ELEMENT_ID + "\" was already taken"); } searchresultsElem.setAttribute("id", SEARCH_RESULTS_ELEMENT_ID); searchresultsElem.setAttribute(A_DOCGEN_RANK, E_SECTION); // Docgen templates may expect page-elements to have a title: Element titleElem = doc.createElementNS(XMLNS_DOCBOOK5, E_TITLE); titleElem.setTextContent(SEARCH_RESULTS_PAGE_TITLE); searchresultsElem.appendChild(titleElem); } // We must add it to the document so that .node?root and such will work. doc.getDocumentElement().appendChild(searchresultsElem); try { TOCNode searchresultsTOCNode = new TOCNode(searchresultsElem, 0); searchresultsTOCNode.setFileElement(true); searchresultsTOCNode.setOutputFileName(FILE_SEARCH_RESULTS_HTML); currentFileTOCNode = searchresultsTOCNode; generateHTMLFile_inner(dataModel, currentFileTOCNode.getOutputFileName()); } finally { doc.getDocumentElement().removeChild(searchresultsElem); } } private void generateHTMLFile_inner(SimpleHash dataModel, String fileName) throws TemplateException, IOException { Template template = fmConfig.getTemplate("page.ftlh"); File outputFile = new File(destDir, fileName); FileOutputStream fos = new FileOutputStream(outputFile); OutputStreamWriter osw = new OutputStreamWriter(fos, UTF_8); Writer writer = new BufferedWriter(osw, 2048); try { template.process( dataModel, writer, null, NodeModel.wrap(currentFileTOCNode.getElement())); } finally { writer.close(); } } private int countTOFEntries(TOCNode parent, int displayDepth) { int sum = 0; TOCNode child = parent.getFirstChild(); while (child != null) { if (child.isFileElement()) { sum++; if (displayDepth > 1) { sum += countTOFEntries(child, displayDepth - 1); } } child = child.getNext(); } return sum; } /** * Checks if a document-structure-element has top-level content. * Top-level content is visible content that is outside the nested * document-structure-element-s that have enough rank to get into the * Page Contents table. */ private boolean hasTopLevelContent(Element element) { for (Element elem : XMLUtil.childrenElementsOf(element)) { if (elem.getNamespaceURI().equals(XMLNS_DOCBOOK5)) { if (elem.hasAttribute(A_DOCGEN_FILE_ELEMENT) || elem.hasAttribute(A_DOCGEN_PAGE_TOC_ELEMENT)) { return false; } String name = elem.getLocalName(); if (!name.equals(E_TITLE) && !name.equals(E_SUBTITLE) && !name.equals(E_INFO) && !name.equals(E_FOOTNOTE)) { if (VISIBLE_TOPLEVEL_ELEMENTS.contains(name)) { return true; } } } } return false; } private String createElementLinkURL(final Element elem) throws DocgenException { if (elem.hasAttribute(A_DOCGEN_NOT_ADDRESSABLE)) { return null; } String extLink = getExternalLinkTOCNodeURLOrNull(elem); if (extLink != null) { return extLink; } // Find the closest id: String id = null; Node node = elem; while (node != null) { if (node instanceof Element) { id = XMLUtil.getAttribute((Element) node, "id"); if (id != null) { break; } } node = node.getParentNode(); } if (id == null) { throw new DocgenException( "Can't create link for the \"" + elem.getLocalName() + "\" element: Nor this element nor its ascendants have an " + "id."); } final Element idElem = (Element) node; String fileName = null; Element curElem = idElem; do { TOCNode fileTOCNode = getFileTOCNodeFor(curElem); if (fileTOCNode == null) { curElem = (Element) curElem.getParentNode(); } else { fileName = fileTOCNode.getOutputFileName(); } } while (fileName == null); String link; if (currentFileTOCNode != null && fileName.equals(currentFileTOCNode.getOutputFileName())) { link = ""; } else { link = fileName; } if (getFileTOCNodeFor(idElem) == null) { link = link + "#" + id; } // IE6 doesn't like empty href-s: if (link.length() == 0) { link = fileName; } return link; } private String getArgString(List<?> args, int argIdx) throws TemplateModelException { Object value = args.get(argIdx); if (value instanceof TemplateScalarModel) { return ((TemplateScalarModel) value).getAsString(); } if (value instanceof TemplateModel) { throw new TemplateModelException("Argument #" + (argIdx + 1) + " should be a string, but it was: " + ClassUtil.getFTLTypeDescription((TemplateModel) value)); } throw new IllegalArgumentException("\"value\" must be " + TemplateModel.class.getName()); } private TemplateMethodModelEx createLinkFromID = new TemplateMethodModelEx() { public Object exec(@SuppressWarnings("rawtypes") final List args) throws TemplateModelException { if (args.size() != 1) { throw new TemplateModelException( "Method CreateLinkFromID should have exactly one " + "parameter."); } String id = getArgString(args, 0); try { return createLinkFromId(id); } catch (DocgenException e) { throw new TemplateModelException("Can't resolve id " + StringUtil.jQuote(id) + " to URL", e); } } }; private String createLinkFromId(String id) throws DocgenException { Element elem = elementsById.get(id); if (elem == null) { throw new DocgenException( "No element exists with this id: \"" + id + "\""); } return createElementLinkURL(elem); } private TemplateMethodModelEx createLinkFromNode = new TemplateMethodModelEx() { public Object exec(@SuppressWarnings("rawtypes") final List args) throws TemplateModelException { if (args.size() != 1) { throw new TemplateModelException( "Method CreateLinkFromNode should have exactly one " + "parameter."); } Object arg1 = args.get(0); if (!(arg1 instanceof NodeModel)) { throw new TemplateModelException( "The first parameter to CreateLinkFromNode must be a " + "node, but it wasn't. (Class: " + arg1.getClass().getName() + ")"); } Node node = ((NodeModel) arg1).getNode(); if (!(node instanceof Element)) { throw new TemplateModelException( "The first parameter to CreateLinkFromNode must be an " + "element node, but it wasn't. (Class: " + arg1.getClass().getName() + ")"); } try { String url = createElementLinkURL((Element) node); return url != null ? new SimpleScalar(url) : null; } catch (DocgenException e) { throw new TemplateModelException( "CreateLinkFromNode falied to create link.", e); } } }; private TemplateMethodModelEx nodeFromID = new TemplateMethodModelEx() { public Object exec(@SuppressWarnings("rawtypes") List args) throws TemplateModelException { Node node = elementsById.get(getArgString(args, 0)); return NodeModel.wrap(node); } }; // ------------------------------------------------------------------------- public File getDestinationDirectory() { return destDir; } /** * Sets the directory where all the output files will go. */ public void setDestinationDirectory(File destDir) { this.destDir = destDir; } public File getSourceDirectory() { return srcDir; } public void setSourceDirectory(File srcDir) { this.srcDir = srcDir; } public Boolean getOffline() { return offline; } public void setOffline(Boolean offline) { this.offline = offline; } public boolean getSimpleNavigationMode() { return simpleNavigationMode; } public void setSimpleNavigationMode(boolean simpleNavigationMode) { this.simpleNavigationMode = simpleNavigationMode; } public boolean getShowEditoralNotes() { return showEditoralNotes; } public void setShowEditoralNotes(boolean showEditoralNotes) { this.showEditoralNotes = showEditoralNotes; } public boolean getValidate() { return validate; } /** * Specifies if the DocBook XML should be validated against the DocBook 5 * RELAX NG Schema; defaults to {@code true}. Setting this to {@code false} * can have whatever random effects later if the DocBook isn't valid, * since the transformation written with the assumption that source is * valid XML. */ public void setValidate(boolean validate) { this.validate = validate; } public TimeZone getTimeZone() { return timeZone; } public void setTimeZone(TimeZone timeZone) { this.timeZone = timeZone; } public boolean getPrintProgress() { return printProgress; } /** * Sets if {@link #execute()} should print feedback to the stdout. * Note that errors (exceptions) will never be printed, just thrown. */ public void setPrintProgress(boolean printProgress) { this.printProgress = printProgress; } public boolean getGenerateEclipseToC() { return generateEclipseTOC; } public void setGenerateEclipseToC(boolean eclipseToC) { this.generateEclipseTOC = eclipseToC; } // ------------------------------------------------------------------------- /** * A node in the XML document for which a ToC entry should be shown. * These nodes form a tree that exists in parallel with the the tree of DOM * nodes. */ public class TOCNode { private final Element element; private final int traversalIndex; private TOCNode parent; private TOCNode next; private TOCNode previous; private TOCNode firstChild; private TOCNode lastChild; private boolean fileElement; private String outputFileName; public TOCNode(Element element, int traversalIndex) { this.element = element; this.traversalIndex = traversalIndex; } public TOCNode getFirstChild() { return firstChild; } public void setFirstChild(TOCNode firstChild) { this.firstChild = firstChild; } public TOCNode getLastChild() { return lastChild; } public void setLastChild(TOCNode lastChild) { this.lastChild = lastChild; } public void setParent(TOCNode parent) { this.parent = parent; } public TOCNode getNext() { return next; } public void setNext(TOCNode next) { this.next = next; } public TOCNode getPrevious() { return previous; } public void setPrevious(TOCNode previous) { this.previous = previous; } public TOCNode getParent() { return parent; } public void setOutputFileName(String outputFileName) { if (!fileElement) { throw new BugException("Can't set outputFileName before setting fileElement to true"); } this.outputFileName = outputFileName; } /** * {@code null} if no file will be generated for this node, despite its "rank". This is the case for nodes that * are external links, or when {@link Transform#simpleNavigationMode} is {@code true} and the file would only * contain a ToC. */ public String getOutputFileName() { return outputFileName; } public Element getElement() { return element; } public void setFileElement(boolean fileElement) { this.fileElement = fileElement; } public boolean isFileElement() { return fileElement; } public String theSomethingElement() { return XMLUtil.theSomethingElement(element); } public TOCNode getNextInTraversarOrder() { return traversalIndex + 1 < tocNodes.size() ? tocNodes.get(traversalIndex + 1) : null; } public TOCNode getPreviousInTraversarOrder() { return traversalIndex > 0 ? tocNodes.get(traversalIndex - 1) : null; } } enum DocumentStructureRank { SIMPLESECT, SECTION3, SECTION2, SECTION1, CHAPTER, PART, BOOK; @Override public String toString() { return name().toLowerCase(); } static String sectionToString(int level) { return DocumentStructureRank.SECTION1.toString().substring( 0, DocumentStructureRank.SECTION1.toString().length() - 1) + level; } } }