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}