001/* 002 * Copyright (c) 2003, 2022, Oracle and/or its affiliates. All rights reserved. 003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 004 * 005 * This code is free software; you can redistribute it and/or modify it 006 * under the terms of the GNU General Public License version 2 only, as 007 * published by the Free Software Foundation. Oracle designates this 008 * particular file as subject to the "Classpath" exception as provided 009 * by Oracle in the LICENSE file that accompanied this code. 010 * 011 * This code is distributed in the hope that it will be useful, but WITHOUT 012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 013 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 014 * version 2 for more details (a copy is included in the LICENSE file that 015 * accompanied this code). 016 * 017 * You should have received a copy of the GNU General Public License version 018 * 2 along with this work; if not, write to the Free Software Foundation, 019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 020 * 021 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 022 * or visit www.oracle.com if you need additional information or have any 023 * questions. 024 */ 025 026package org.jdrupes.mdoclet.internal.doclets.formats.html.markup; 027 028import java.io.IOException; 029import java.io.Writer; 030import java.time.ZonedDateTime; 031import java.time.format.DateTimeFormatter; 032import java.util.ArrayList; 033import java.util.Arrays; 034import java.util.List; 035import java.util.Locale; 036 037import org.jdrupes.mdoclet.internal.doclets.toolkit.Content; 038import org.jdrupes.mdoclet.internal.doclets.toolkit.util.DocPath; 039import org.jdrupes.mdoclet.internal.doclets.toolkit.util.DocPaths; 040 041/** 042 * An HTML {@code <head>} element. 043 * 044 * Many methods return the current object, to facilitate fluent builder-style usage. 045 */ 046public class Head extends Content { 047 private final Runtime.Version docletVersion; 048 private final ZonedDateTime generatedDate; 049 private final DocPath pathToRoot; 050 private String title; 051 private String charset; 052 private final List<String> keywords; 053 private String description; 054 private String generator; 055 private boolean showTimestamp; 056 private DocPath mainStylesheet; 057 private List<DocPath> additionalStylesheets = List.of(); 058 private boolean index; 059 private Script mainBodyScript; 060 private final List<Script> scripts; 061 // Scripts added via --add-script option 062 private List<DocPath> additionalScripts = List.of(); 063 private final List<Content> extraContent; 064 private boolean addDefaultScript = true; 065 private DocPath canonicalLink; 066 067 /** 068 * Creates a {@code Head} object, for a given file and HTML version. 069 * The file is used to help determine the relative paths to stylesheet and script files. 070 * The HTML version is used to determine the appropriate form of a META element 071 * recording the time the file was created. 072 * The doclet version should also be provided for recording in the file. 073 * @param path the path for the file that will include this HEAD element 074 * @param docletVersion the doclet version 075 */ 076 public Head(DocPath path, Runtime.Version docletVersion, 077 ZonedDateTime generatedDate) { 078 this.docletVersion = docletVersion; 079 this.generatedDate = generatedDate; 080 pathToRoot = path.parent().invert(); 081 keywords = new ArrayList<>(); 082 scripts = new ArrayList<>(); 083 extraContent = new ArrayList<>(); 084 } 085 086 /** 087 * Sets the title to appear in the TITLE element. 088 * 089 * @param title the title 090 * @return this object 091 */ 092 public Head setTitle(String title) { 093 this.title = title; 094 return this; 095 } 096 097 /** 098 * Sets the charset to be declared in a META {@code Content-TYPE} element. 099 * 100 * @param charset the charset 101 * @return this object 102 */ 103 // For temporary compatibility, this is currently optional. 104 // Eventually, this should be a required call. 105 public Head setCharset(String charset) { 106 this.charset = charset; 107 return this; 108 } 109 110 /** 111 * Sets the content for the description META element. 112 */ 113 public Head setDescription(String description) { 114 this.description = description; 115 return this; 116 } 117 118 /** 119 * Sets the content for the generator META element. 120 */ 121 public Head setGenerator(String generator) { 122 this.generator = generator; 123 return this; 124 } 125 126 /** 127 * Adds a list of keywords to appear in META {@code keywords} elements. 128 * 129 * @param keywords the list of keywords, or null if none need to be added 130 * @return this object 131 */ 132 public Head addKeywords(List<String> keywords) { 133 if (keywords != null) { 134 this.keywords.addAll(keywords); 135 } 136 return this; 137 } 138 139 /** 140 * Sets whether or not timestamps should be recorded in the HEAD element. 141 * The timestamp will be recorded in a comment, and in an appropriate META 142 * element, depending on the HTML version specified when this object was created. 143 * 144 * @param timestamp true if timestamps should be be added. 145 * @return this object 146 */ 147 // For temporary backwards compatibility, if this method is not called, 148 // no 'Generated by javadoc' comment will be added. 149 public Head setTimestamp(boolean timestamp) { 150 showTimestamp = timestamp; 151 return this; 152 } 153 154 /** 155 * Sets the main and any additional stylesheets to be listed in the HEAD element. 156 * The paths for the stylesheets must be relative to the root of the generated 157 * documentation hierarchy. 158 * 159 * @param main the main stylesheet, or null to use the default 160 * @param additional a list of any additional stylesheets to be included 161 * @return this object 162 */ 163 public Head setStylesheets(DocPath main, List<DocPath> additional) { 164 this.mainStylesheet = main; 165 this.additionalStylesheets = additional; 166 return this; 167 } 168 169 /** 170 * Sets the list of additional script files to be added to the HEAD element. 171 * The path for the script files must be relative to the root of the generated 172 * documentation hierarchy. 173 * 174 * @param scripts the list of additional script files 175 * @return this object 176 */ 177 public Head setAdditionalScripts(List<DocPath> scripts) { 178 this.additionalScripts = scripts; 179 return this; 180 } 181 182 /** 183 * Sets whether or not to include the supporting scripts and stylesheets for the 184 * "search" feature. 185 * If the feature is enabled, a {@code Script} must be provided into which some 186 * JavaScript code will be injected, to be executed during page loading. The value 187 * will be ignored if the feature is not enabled. 188 * 189 * @param index true if the supporting files are to be included 190 * @param mainBodyScript the {@code Script} object, or null 191 * @return this object 192 */ 193 public Head setIndex(boolean index, Script mainBodyScript) { 194 this.index = index; 195 this.mainBodyScript = mainBodyScript; 196 return this; 197 } 198 199 /** 200 * Adds a script to be included in the HEAD element. 201 * 202 * @param script the script 203 * @return this object 204 */ 205 public Head addScript(Script script) { 206 scripts.add(script); 207 return this; 208 } 209 210 /** 211 * Specifies whether or not to add a reference to a default script to be included 212 * in the HEAD element. 213 * The default script will normally be included; this method may be used to prevent that. 214 * 215 * @param addDefaultScript whether or not a default script will be included 216 * @return this object 217 */ 218 public Head addDefaultScript(boolean addDefaultScript) { 219 this.addDefaultScript = addDefaultScript; 220 return this; 221 } 222 223 /** 224 * Specifies a value for a 225 * <a href="https://en.wikipedia.org/wiki/Canonical_link_element">canonical link</a> 226 * in the {@code <head>} element. 227 * @param link the value for the canonical link 228 */ 229 public void setCanonicalLink(DocPath link) { 230 this.canonicalLink = link; 231 } 232 233 /** 234 * Adds additional content to be included in the HEAD element. 235 * 236 * @param contents the content 237 * @return this object 238 */ 239 public Head addContent(Content... contents) { 240 extraContent.addAll(Arrays.asList(contents)); 241 return this; 242 } 243 244 /** 245 * {@inheritDoc} 246 * 247 * @implSpec This implementation always returns {@code false}. 248 * 249 * @return {@code false} 250 */ 251 @Override 252 public boolean isEmpty() { 253 return false; 254 } 255 256 @Override 257 public boolean write(Writer out, String newline, boolean atNewline) 258 throws IOException { 259 return toContent().write(out, newline, atNewline); 260 } 261 262 /** 263 * Returns the HTML for the HEAD element. 264 * 265 * @return the HTML 266 */ 267 private Content toContent() { 268 var head = new HtmlTree(TagName.HEAD); 269 head.add(getGeneratedBy(showTimestamp, generatedDate)); 270 head.add(HtmlTree.TITLE(title)); 271 272 head.add( 273 HtmlTree.META("viewport", "width=device-width, initial-scale=1")); 274 275 if (charset != null) { // compatibility; should this be allowed? 276 head.add(HtmlTree.META("Content-Type", "text/html", charset)); 277 } 278 279 if (showTimestamp) { 280 DateTimeFormatter dateFormat 281 = DateTimeFormatter.ofPattern("yyyy-MM-dd"); 282 head.add( 283 HtmlTree.META("dc.created", generatedDate.format(dateFormat))); 284 } 285 286 if (description != null) { 287 head.add(HtmlTree.META("description", description)); 288 } 289 290 if (generator != null) { 291 head.add(HtmlTree.META("generator", generator)); 292 } 293 294 for (String k : keywords) { 295 head.add(HtmlTree.META("keywords", k)); 296 } 297 298 if (canonicalLink != null) { 299 var link = new HtmlTree(TagName.LINK); 300 link.put(HtmlAttr.REL, "canonical"); 301 link.put(HtmlAttr.HREF, canonicalLink.getPath()); 302 head.add(link); 303 } 304 305 addStylesheets(head); 306 addScripts(head); 307 extraContent.forEach(head::add); 308 309 return head; 310 } 311 312 private Comment getGeneratedBy(boolean timestamp, ZonedDateTime buildDate) { 313 String text = "Generated by javadoc"; // marker string, deliberately not 314 // localized 315 text += " (" + docletVersion.feature() + ")"; 316 if (timestamp) { 317 DateTimeFormatter fmt 318 = DateTimeFormatter.ofPattern("EEE MMM dd HH:mm:ss zzz yyyy") 319 .withLocale(Locale.US); 320 text += " on " + buildDate.format(fmt); 321 } 322 return new Comment(text); 323 } 324 325 private void addStylesheets(HtmlTree head) { 326 if (mainStylesheet == null) { 327 mainStylesheet = DocPaths.STYLESHEET; 328 } 329 addStylesheet(head, mainStylesheet); 330 331 for (DocPath path : additionalStylesheets) { 332 addStylesheet(head, path); 333 } 334 335 if (index) { 336 addStylesheet(head, 337 DocPaths.SCRIPT_DIR.resolve(DocPaths.JQUERY_UI_CSS)); 338 } 339 } 340 341 private void addStylesheet(HtmlTree head, DocPath stylesheet) { 342 head.add(HtmlTree.LINK("stylesheet", "text/css", 343 pathToRoot.resolve(stylesheet).getPath(), "Style")); 344 } 345 346 private void addScripts(HtmlTree head) { 347 if (addDefaultScript) { 348 head.add(HtmlTree 349 .SCRIPT(pathToRoot.resolve(DocPaths.JAVASCRIPT).getPath())); 350 } 351 if (index) { 352 if (pathToRoot != null && mainBodyScript != null) { 353 String ptrPath 354 = pathToRoot.isEmpty() ? "." : pathToRoot.getPath(); 355 mainBodyScript.append("var pathtoroot = ") 356 .appendStringLiteral(ptrPath + "/") 357 .append(";\n") 358 .append("loadScripts(document, 'script');"); 359 } 360 addScriptElement(head, DocPaths.JQUERY_JS); 361 addScriptElement(head, DocPaths.JQUERY_UI_JS); 362 } 363 for (DocPath path : additionalScripts) { 364 addScriptElement(head, path); 365 } 366 for (Script script : scripts) { 367 head.add(script.asContent()); 368 } 369 } 370 371 private void addScriptElement(HtmlTree head, DocPath filePath) { 372 DocPath scriptFile 373 = pathToRoot.resolve(DocPaths.SCRIPT_DIR).resolve(filePath); 374 head.add(HtmlTree.SCRIPT(scriptFile.getPath())); 375 } 376}