001/*
002 * Copyright (c) 1998, 2023, 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;
027
028import java.util.List;
029import java.util.ListIterator;
030import java.util.SortedSet;
031import java.util.stream.Stream;
032
033import javax.lang.model.element.Element;
034import javax.lang.model.element.ElementKind;
035import javax.lang.model.element.ModuleElement;
036import javax.lang.model.element.PackageElement;
037import javax.lang.model.element.TypeElement;
038
039import org.jdrupes.mdoclet.internal.doclets.formats.html.Navigation.PageMode;
040import org.jdrupes.mdoclet.internal.doclets.formats.html.markup.BodyContents;
041import org.jdrupes.mdoclet.internal.doclets.formats.html.markup.ContentBuilder;
042import org.jdrupes.mdoclet.internal.doclets.formats.html.markup.Entity;
043import org.jdrupes.mdoclet.internal.doclets.formats.html.markup.HtmlStyle;
044import org.jdrupes.mdoclet.internal.doclets.formats.html.markup.HtmlTree;
045import org.jdrupes.mdoclet.internal.doclets.formats.html.markup.TagName;
046import org.jdrupes.mdoclet.internal.doclets.formats.html.markup.Text;
047import org.jdrupes.mdoclet.internal.doclets.toolkit.Content;
048import org.jdrupes.mdoclet.internal.doclets.toolkit.util.DocFileIOException;
049import org.jdrupes.mdoclet.internal.doclets.toolkit.util.DocPath;
050import org.jdrupes.mdoclet.internal.doclets.toolkit.util.DocPaths;
051import org.jdrupes.mdoclet.internal.doclets.toolkit.util.IndexBuilder;
052import org.jdrupes.mdoclet.internal.doclets.toolkit.util.IndexItem;
053
054import com.sun.source.doctree.DeprecatedTree;
055
056/**
057 * Generator for either a single index or split index for all
058 * documented elements, terms defined in some documentation comments,
059 * and summary pages.
060 *
061 * @see IndexBuilder
062 */
063public class IndexWriter extends HtmlDocletWriter {
064
065    protected final IndexBuilder mainIndex;
066    protected final boolean splitIndex;
067
068    /**
069     * Generates the main index of all documented elements, terms defined in some documentation
070     * comments, and summary pages.
071     *
072     * If {@link HtmlOptions#splitIndex()} is true, a separate page is generated for each
073     * initial letter; otherwise, a single page is generated for all items in the index.
074     *
075     * @param configuration the configuration
076     * @throws DocFileIOException if an error occurs while writing the files
077     */
078    public static void generate(HtmlConfiguration configuration)
079            throws DocFileIOException {
080        IndexBuilder mainIndex = configuration.mainIndex;
081        List<Character> firstCharacters = mainIndex.getFirstCharacters();
082        if (configuration.getOptions().splitIndex()) {
083            ListIterator<Character> iter = firstCharacters.listIterator();
084            while (iter.hasNext()) {
085                Character ch = iter.next();
086                DocPath file = DocPaths.INDEX_FILES
087                    .resolve(DocPaths.indexN(iter.nextIndex()));
088                IndexWriter writer = new IndexWriter(configuration, file);
089                writer.generateIndexFile(firstCharacters, List.of(ch));
090            }
091        } else {
092            IndexWriter writer
093                = new IndexWriter(configuration, DocPaths.INDEX_ALL);
094            writer.generateIndexFile(firstCharacters, firstCharacters);
095        }
096    }
097
098    /**
099     * Creates a writer that can write a page containing some or all of the overall index.
100     *
101     * @param configuration  the current configuration
102     * @param path           the file to be generated
103     */
104    protected IndexWriter(HtmlConfiguration configuration, DocPath path) {
105        super(configuration, path);
106        this.mainIndex = configuration.mainIndex;
107        this.splitIndex = configuration.getOptions().splitIndex();
108    }
109
110    /**
111     * Generates a page containing some or all of the overall index.
112     *
113     * @param allFirstCharacters     the initial characters of all index items
114     * @param displayFirstCharacters the initial characters of the index items to appear on this page
115     * @throws DocFileIOException if an error occurs while writing the page
116     */
117    protected void generateIndexFile(List<Character> allFirstCharacters,
118            List<Character> displayFirstCharacters) throws DocFileIOException {
119        String title = splitIndex
120            ? resources.getText("doclet.Window_Split_Index",
121                displayFirstCharacters.get(0))
122            : resources.getText("doclet.Window_Single_Index");
123        HtmlTree body = getBody(getWindowTitle(title));
124        Content mainContent = new ContentBuilder();
125        addLinksForIndexes(allFirstCharacters, mainContent);
126        for (Character ch : displayFirstCharacters) {
127            addContents(ch, mainIndex.getItems(ch), mainContent);
128        }
129        addLinksForIndexes(allFirstCharacters, mainContent);
130        body.add(new BodyContents()
131            .setHeader(getHeader(PageMode.INDEX))
132            .addMainContent(HtmlTree.DIV(HtmlStyle.header,
133                HtmlTree.HEADING(Headings.PAGE_TITLE_HEADING,
134                    contents.getContent("doclet.Index"))))
135            .addMainContent(mainContent)
136            .setFooter(getFooter()));
137
138        String description
139            = splitIndex ? "index: " + displayFirstCharacters.get(0) : "index";
140        printHtmlDocument(null, description, body);
141    }
142
143    /**
144     * Adds a set of items to the page.
145     *
146     * @param ch      the first character of the names of the items
147     * @param items   the items
148     * @param content the content to which to add the items
149     */
150    protected void addContents(char ch, SortedSet<IndexItem> items,
151            Content content) {
152        addHeading(ch, content);
153
154        var dl = HtmlTree.DL(HtmlStyle.index);
155        for (IndexItem item : items) {
156            addDescription(item, dl);
157        }
158        content.add(dl);
159    }
160
161    /**
162     * Adds a heading containing the first character for a set of items.
163     *
164     * @param ch      the first character of the names of the items
165     * @param content the content to which to add the items
166     */
167    protected void addHeading(char ch, Content content) {
168        Content headContent = Text.of(String.valueOf(ch));
169        var heading = HtmlTree
170            .HEADING(Headings.CONTENT_HEADING, HtmlStyle.title, headContent)
171            .setId(HtmlIds.forIndexChar(ch));
172        content.add(heading);
173    }
174
175    /**
176     * Adds the description for an index item into a list.
177     *
178     * @param indexItem the item
179     * @param dl        the list
180     */
181    protected void addDescription(IndexItem indexItem, Content dl) {
182        if (indexItem.isTagItem()) {
183            addTagDescription(indexItem, dl);
184        } else if (indexItem.isElementItem()) {
185            addElementDescription(indexItem, dl);
186        }
187    }
188
189    /**
190     * Add one line summary comment for the item.
191     *
192     * @param item the item to be documented
193     * @param target the content to which the description will be added
194     */
195    protected void addElementDescription(IndexItem item, Content target) {
196        Content dt;
197        Element element = item.getElement();
198        String label = item.getLabel();
199        switch (element.getKind()) {
200        case MODULE:
201            dt = HtmlTree
202                .DT(getModuleLink((ModuleElement) element, Text.of(label)));
203            dt.add(" - ").add(contents.module_).add(" " + label);
204            break;
205
206        case PACKAGE:
207            dt = HtmlTree
208                .DT(getPackageLink((PackageElement) element, Text.of(label)));
209            if (configuration.showModules) {
210                item.setContainingModule(utils
211                    .getFullyQualifiedName(utils.containingModule(element)));
212            }
213            dt.add(" - ").add(contents.package_).add(" " + label);
214            break;
215
216        case CLASS:
217        case ENUM:
218        case RECORD:
219        case ANNOTATION_TYPE:
220        case INTERFACE:
221            dt = HtmlTree.DT(getLink(new HtmlLinkInfo(configuration,
222                HtmlLinkInfo.Kind.SHOW_TYPE_PARAMS_IN_LABEL,
223                (TypeElement) element).style(HtmlStyle.typeNameLink)));
224            dt.add(" - ");
225            addClassInfo((TypeElement) element, dt);
226            break;
227
228        case CONSTRUCTOR:
229        case METHOD:
230        case FIELD:
231        case ENUM_CONSTANT:
232            TypeElement containingType = item.getContainingTypeElement();
233            dt = HtmlTree
234                .DT(getDocLink(HtmlLinkInfo.Kind.PLAIN, containingType, element,
235                    label, HtmlStyle.memberNameLink));
236            dt.add(" - ");
237            addMemberDesc(element, containingType, dt);
238            break;
239
240        default:
241            throw new Error();
242        }
243        target.add(dt);
244        var dd = new HtmlTree(TagName.DD);
245        if (element.getKind() == ElementKind.MODULE
246            || element.getKind() == ElementKind.PACKAGE) {
247            addSummaryComment(element, dd);
248        } else {
249            addComment(element, dd);
250        }
251        target.add(dd);
252    }
253
254    /**
255     * Adds information for the given type element.
256     *
257     * @param te      the element
258     * @param content the content to which the class info will be added
259     */
260    protected void addClassInfo(TypeElement te, Content content) {
261        content.add(contents.getContent("doclet.in",
262            utils.getTypeElementKindName(te, false),
263            getPackageLink(utils.containingPackage(te),
264                getLocalizedPackageName(utils.containingPackage(te)))));
265    }
266
267    /**
268     * Adds a description for an item found in a documentation comment.
269     *
270     * @param item   the item
271     * @param target the list to which to add the description
272     */
273    protected void addTagDescription(IndexItem item, Content target) {
274        String itemPath
275            = pathToRoot.isEmpty() ? "" : pathToRoot.getPath() + "/";
276        itemPath += item.getUrl();
277        var labelLink = HtmlTree.A(itemPath, Text.of(item.getLabel()));
278        var dt = HtmlTree.DT(labelLink.setStyle(HtmlStyle.searchTagLink));
279        dt.add(" - ");
280        dt.add(contents.getContent("doclet.Search_tag_in", item.getHolder()));
281        target.add(dt);
282        var dd = new HtmlTree(TagName.DD);
283        if (item.getDescription().isEmpty()) {
284            dd.add(Entity.NO_BREAK_SPACE);
285        } else {
286            dd.add(item.getDescription());
287        }
288        target.add(dd);
289    }
290
291    /**
292     * Adds a comment for an element in the index. If the element is deprecated
293     * and it has a @deprecated tag, use that comment; otherwise,  if the containing
294     * class for this element is deprecated, then add the word "Deprecated." at
295     * the start and then print the normal comment.
296     *
297     * @param element     the element
298     * @param content the content to which the comment will be added
299     */
300    protected void addComment(Element element, Content content) {
301        var span = HtmlTree.SPAN(HtmlStyle.deprecatedLabel,
302            getDeprecatedPhrase(element));
303        var div = HtmlTree.DIV(HtmlStyle.deprecationBlock);
304        if (utils.isDeprecated(element)) {
305            div.add(span);
306            List<? extends DeprecatedTree> tags
307                = utils.getDeprecatedTrees(element);
308            if (!tags.isEmpty())
309                addInlineDeprecatedComment(element, tags.get(0), div);
310            content.add(div);
311        } else {
312            TypeElement encl = utils.getEnclosingTypeElement(element);
313            while (encl != null) {
314                if (utils.isDeprecated(encl)) {
315                    div.add(span);
316                    content.add(div);
317                    break;
318                }
319                encl = utils.getEnclosingTypeElement(encl);
320            }
321            addSummaryComment(element, content);
322        }
323    }
324
325    /**
326     * Adds a description for a member element.
327     *
328     * @param member    the element
329     * @param enclosing the enclosing type element
330     * @param content   the content to which the member description will be added
331     */
332    protected void addMemberDesc(Element member, TypeElement enclosing,
333            Content content) {
334        String kindName = utils.getTypeElementKindName(enclosing, true);
335        String resource = switch (member.getKind()) {
336        case ENUM_CONSTANT -> "doclet.Enum_constant_in";
337        case FIELD -> utils.isStatic(member) ? "doclet.Static_variable_in"
338            : "doclet.Variable_in";
339        case CONSTRUCTOR -> "doclet.Constructor_for";
340        case METHOD -> utils.isAnnotationInterface(enclosing)
341            ? "doclet.Element_in"
342            : utils.isStatic(member) ? "doclet.Static_method_in"
343                : "doclet.Method_in";
344        case RECORD_COMPONENT -> "doclet.Record_component_in";
345        default -> throw new IllegalArgumentException(
346            member.getKind().toString());
347        };
348        content.add(contents.getContent(resource, kindName)).add(" ");
349        addPreQualifiedClassLink(HtmlLinkInfo.Kind.PLAIN, enclosing,
350            null, content);
351    }
352
353    /**
354     * Add links for all the index files, based on the first character of the names of the items.
355     *
356     * @param allFirstCharacters the list of all first characters to be linked
357     * @param content            the content to which the links for indexes will be added
358     */
359    protected void addLinksForIndexes(List<Character> allFirstCharacters,
360            Content content) {
361        ListIterator<Character> iter = allFirstCharacters.listIterator();
362        while (iter.hasNext()) {
363            char ch = iter.next();
364            Content label = Text.of(Character.toString(ch));
365            Content link = splitIndex
366                ? links.createLink(DocPaths.indexN(iter.nextIndex()), label)
367                : links.createLink(HtmlIds.forIndexChar(ch), label);
368            content.add(link);
369            content.add(Entity.NO_BREAK_SPACE);
370        }
371
372        content.add(new HtmlTree(TagName.BR));
373        List<Content> pageLinks = Stream.of(IndexItem.Category.values())
374            .flatMap(c -> mainIndex.getItems(c).stream())
375            .filter(i -> !(i.isElementItem() || i.isTagItem()))
376            .sorted(
377                (i1, i2) -> utils.compareStrings(i1.getLabel(), i2.getLabel()))
378            .map(i -> links.createLink(pathToRoot.resolve(i.getUrl()),
379                contents.getNonBreakString(i.getLabel())))
380            .toList();
381        content.add(contents.join(getVerticalSeparator(), pageLinks));
382    }
383
384    private Content getVerticalSeparator() {
385        return HtmlTree.SPAN(HtmlStyle.verticalSeparator, Text.of("|"));
386    }
387}