001/*
002 * JDrupes MDoclet
003 * Copyright (C) 2021 Michael N. Lipp
004 * 
005 * This program is free software; you can redistribute it and/or modify it 
006 * under the terms of the GNU Affero General Public License as published by 
007 * the Free Software Foundation; either version 3 of the License, or 
008 * (at your option) any later version.
009 *
010 * This program is distributed in the hope that it will be useful, but 
011 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
012 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 
013 * for more details.
014 *
015 * You should have received a copy of the GNU Affero General Public License along 
016 * with this program; if not, see <http://www.gnu.org/licenses/>.
017 */
018
019package org.jdrupes.mdoclet;
020
021import com.sun.source.doctree.AttributeTree.ValueKind;
022import com.sun.source.doctree.AuthorTree;
023import com.sun.source.doctree.DeprecatedTree;
024import com.sun.source.doctree.DocTree;
025import com.sun.source.doctree.EndElementTree;
026import com.sun.source.doctree.ErroneousTree;
027import com.sun.source.doctree.LiteralTree;
028import com.sun.source.doctree.ParamTree;
029import com.sun.source.doctree.ReferenceTree;
030import com.sun.source.doctree.ReturnTree;
031import com.sun.source.doctree.SeeTree;
032import com.sun.source.doctree.SinceTree;
033import com.sun.source.doctree.StartElementTree;
034import com.sun.source.doctree.TextTree;
035import com.sun.source.doctree.ThrowsTree;
036import com.sun.source.doctree.VersionTree;
037import com.sun.source.util.DocTreeFactory;
038import com.sun.source.util.SimpleDocTreeVisitor;
039import java.util.ArrayList;
040import java.util.List;
041import java.util.regex.Matcher;
042import java.util.regex.Pattern;
043
044import javax.lang.model.util.Elements;
045
046import org.jsoup.Jsoup;
047import org.jsoup.nodes.Element;
048import org.jsoup.nodes.Node;
049import org.jsoup.nodes.TextNode;
050
051public class TreeConverter {
052
053    private static final Pattern SCAN_RE
054        = Pattern.compile("««@([0-9]+)»»");
055
056    private MarkdownProcessor processor;
057    private DocTreeFactory docTreeFactory;
058    private Elements elements;
059
060    public TreeConverter(MarkdownProcessor processor,
061            DocTreeFactory docTreeFactory, Elements elements) {
062        this.processor = processor;
063        this.docTreeFactory = docTreeFactory;
064        this.elements = elements;
065    }
066
067    private String toMarkdownSource(List<DocTree> specials,
068            List<? extends DocTree> tree) {
069        SimpleDocTreeVisitor<Void, StringBuilder> v
070            = new SimpleDocTreeVisitor<>() {
071
072                @Override
073                public Void visitText(TextTree node, StringBuilder sb) {
074                    sb.append(node.toString());
075                    return null;
076                }
077
078                /**
079                 * Parsing is done, literals can be converted to text.
080                 * 
081                 * {@inheritDoc}
082                 */
083                @Override
084                public Void visitLiteral(LiteralTree node, StringBuilder sb) {
085                    sb.append(node.getBody().toString());
086                    return null;
087                }
088
089                @Override
090                public Void visitStartElement(StartElementTree node,
091                        StringBuilder sb) {
092                    sb.append(node.toString());
093                    return null;
094                }
095
096                @Override
097                public Void visitEndElement(EndElementTree node,
098                        StringBuilder sb) {
099                    sb.append(node.toString());
100                    return null;
101                }
102
103                @Override
104                public Void visitErroneous(ErroneousTree node,
105                        StringBuilder sb) {
106                    sb.append(node.toString());
107                    return null;
108                }
109
110                @Override
111                protected Void defaultAction(DocTree node, StringBuilder sb) {
112                    sb.append("««@" + specials.size() + "»»");
113                    specials.add(node);
114                    return null;
115                }
116            };
117        StringBuilder sb = new StringBuilder();
118        v.visit(tree, sb);
119        return sb.toString();
120    }
121
122    private List<DocTree> mdOutToDocTrees(List<DocTree> specials,
123            String htmlText) {
124        // Re-insert specials
125        List<DocTree> replacement = new ArrayList<>();
126        Matcher matcher = SCAN_RE.matcher(htmlText);
127        int emittedUpTo = 0;
128        while (matcher.find()) {
129            if (matcher.start() > emittedUpTo) {
130                replacement.add(docTreeFactory.newTextTree(
131                    htmlText.substring(emittedUpTo, matcher.start())));
132            }
133            try {
134                int idx = Integer.parseInt(matcher.group(1));
135                replacement.add(specials.get(idx));
136            } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
137            }
138            emittedUpTo = matcher.end();
139        }
140        if (htmlText.length() > emittedUpTo) {
141            replacement.add(docTreeFactory.newTextTree(
142                htmlText.substring(emittedUpTo, htmlText.length())));
143        }
144        return replacement;
145    }
146
147    /**
148     * Converts a complete description.
149     * 
150     * @param tree the tree to convert
151     * @return the result
152     */
153    @SuppressWarnings("unchecked")
154    public List<DocTree> convertDescription(List<? extends DocTree> tree) {
155        if (tree.isEmpty()) {
156            return (List<DocTree>) tree;
157        }
158        List<DocTree> specials = new ArrayList<>();
159        String markdownSource = toMarkdownSource(specials, tree);
160        String transformed = processor.toHtml(markdownSource);
161        List<DocTree> replacement = mdOutToDocTrees(specials, transformed);
162        return replacement;
163    }
164
165    /**
166     * Converts a fragment such as the description of a tag. An attempt is
167     * made to remove any surrounding HTML tag added by the markdown
168     * processor.
169     * 
170     * @param tree the tree to convert
171     * @return the result
172     */
173    @SuppressWarnings("unchecked")
174    public List<DocTree> convertFragment(List<? extends DocTree> tree) {
175        if (tree.isEmpty()) {
176            return (List<DocTree>) tree;
177        }
178        List<DocTree> specials = new ArrayList<>();
179        String markdownSource = toMarkdownSource(specials, tree);
180        String transformed = processor.toHtmlFragment(markdownSource);
181        List<DocTree> replacement = mdOutToDocTrees(specials, transformed);
182        return replacement;
183    }
184
185    /**
186     * Converts a "see" tag. The text is interpreted as markdown.
187     * If the result is a quoted link, only the link is returned.
188     * 
189     * @param tree the tree to convert
190     * @return the result
191     */
192    public List<? extends DocTree>
193            convertSeeFragment(List<? extends DocTree> tree) {
194        if (tree.isEmpty()) {
195            return tree;
196        }
197        List<DocTree> specials = new ArrayList<>();
198        String markdownSource = toMarkdownSource(specials, tree);
199        String transformed = processor.toHtmlFragment(markdownSource).trim();
200        Element target = Jsoup.parseBodyFragment(transformed).body();
201        var childNodes = target.childNodes();
202        if (childNodes.get(0) instanceof TextNode
203            && "“".equals(((TextNode) childNodes.get(0)).text())
204            && childNodes.get(1) instanceof Element
205            && childNodes.get(2) instanceof TextNode
206            && "”".equals(((TextNode) childNodes.get(2)).text())) {
207            return nodeToDocTree((Element) childNodes.get(1));
208        }
209        return childrenToDocTree(target);
210    }
211
212    private List<DocTree> childrenToDocTree(Element root) {
213        var result = new ArrayList<DocTree>();
214        for (var node : root.childNodes()) {
215            result.addAll(nodeToDocTree(node));
216        }
217        return result;
218    }
219
220    private List<DocTree> nodeToDocTree(Node node) {
221        var result = new ArrayList<DocTree>();
222        if (node instanceof TextNode) {
223            result.add(docTreeFactory.newTextTree(((TextNode) node).text()));
224        } else if (node instanceof Element) {
225            var element = (Element) node;
226            var tag = elements.getName(element.tagName());
227            var attrs = new ArrayList<DocTree>();
228            for (var attr : element.attributes()) {
229                attrs.add(docTreeFactory.newAttributeTree(
230                    elements.getName(attr.getKey()), ValueKind.DOUBLE,
231                    List.of(docTreeFactory.newTextTree(attr.getValue()))));
232            }
233            var start = docTreeFactory.newStartElementTree(
234                tag, attrs, false);
235            result.add(start);
236            result.addAll(childrenToDocTree(element));
237            result.add(docTreeFactory.newEndElementTree(tag));
238        }
239        return result;
240    }
241
242    /**
243     * Default conversion is a noop. 
244     * 
245     * @param target the list to append the result to 
246     * @param tree the tree to convert
247     */
248    public void convertTag(List<DocTree> target, DocTree tree) {
249        // Late binding doesn't work with interfaces, sigh...
250        if (tree instanceof AuthorTree) {
251            convertTag(target, (AuthorTree) tree);
252            return;
253        }
254        if (tree instanceof DeprecatedTree) {
255            convertTag(target, (DeprecatedTree) tree);
256            return;
257        }
258        if (tree instanceof ParamTree) {
259            convertTag(target, (ParamTree) tree);
260            return;
261        }
262        if (tree instanceof ReturnTree) {
263            convertTag(target, (ReturnTree) tree);
264            return;
265        }
266        if (tree instanceof SeeTree) {
267            convertTag(target, (SeeTree) tree);
268            return;
269        }
270        if (tree instanceof SinceTree) {
271            convertTag(target, (SinceTree) tree);
272            return;
273        }
274        if (tree instanceof ThrowsTree) {
275            convertTag(target, (ThrowsTree) tree);
276            return;
277        }
278        if (tree instanceof VersionTree) {
279            convertTag(target, (VersionTree) tree);
280            return;
281        }
282        target.add(tree);
283    }
284
285    /**
286     * Converts a {@link AuthorTree}. See the overview for a description
287     * of the behavior. 
288     * 
289     * @param target the list to append the result to 
290     * @param tree the tree to convert
291     */
292    public void convertTag(List<DocTree> target, AuthorTree tree) {
293        target.add(docTreeFactory
294            .newAuthorTree(convertFragment(tree.getName())));
295    }
296
297    /**
298     * Converts a {@link DeprecatedTree}. See the overview for a description
299     * of the behavior. 
300     * 
301     * @param target the list to append the result to 
302     * @param tree the tree to convert
303     */
304    public void convertTag(List<DocTree> target, DeprecatedTree tree) {
305        target.add(docTreeFactory
306            .newDeprecatedTree(convertFragment(tree.getBody())));
307    }
308
309    /**
310     * Converts a {@link ParamTree}. See the overview for a description
311     * of the behavior. 
312     * 
313     * @param target the list to append the result to 
314     * @param tree the tree to convert
315     */
316    public void convertTag(List<DocTree> target, ParamTree tree) {
317        target.add(docTreeFactory.newParamTree(tree.isTypeParameter(),
318            tree.getName(), convertFragment(tree.getDescription())));
319    }
320
321    /**
322     * Converts a {@link ReturnTree}. See the overview for a description
323     * of the behavior. 
324     * 
325     * @param target the list to append the result to 
326     * @param tree the tree to convert
327     */
328    public void convertTag(List<DocTree> target, ReturnTree tree) {
329        target.add(docTreeFactory
330            .newReturnTree(convertFragment(tree.getDescription())));
331    }
332
333    /**
334     * Converts a {@link SeeTree}. See the overview for a description
335     * of the behavior. 
336     * 
337     * @param target the list to append the result to 
338     * @param tree the tree to convert
339     */
340    public void convertTag(List<DocTree> target, SeeTree tree) {
341        if (tree.getReference().size() > 0
342            && (tree.getReference().get(0) instanceof ReferenceTree
343                || tree.getReference().get(0) instanceof StartElementTree)) {
344            target.add(tree);
345            return;
346        }
347        SeeTree newTree = docTreeFactory
348            .newSeeTree(convertSeeFragment(tree.getReference()));
349        target.add(newTree);
350    }
351
352    /**
353     * Converts a {@link SinceTree}. See the overview for a description
354     * of the behavior. 
355     * 
356     * @param target the list to append the result to 
357     * @param tree the tree to convert
358     */
359    public void convertTag(List<DocTree> target, SinceTree tree) {
360        target.add(docTreeFactory.newSinceTree(tree.getBody()));
361    }
362
363    /**
364     * Converts a {@link ThrowsTree}. See the overview for a description
365     * of the behavior. 
366     * 
367     * @param target the list to append the result to 
368     * @param tree the tree to convert
369     */
370    public void convertTag(List<DocTree> target, ThrowsTree tree) {
371        target.add(docTreeFactory.newThrowsTree(tree.getExceptionName(),
372            convertFragment(tree.getDescription())));
373    }
374
375    /**
376     * Converts a {@link VersionTree}. See the overview for a description
377     * of the behavior. 
378     * 
379     * @param target the list to append the result to 
380     * @param tree the tree to convert
381     */
382    public void convertTag(List<DocTree> target, VersionTree tree) {
383        target.add(docTreeFactory.newVersionTree(tree.getBody()));
384    }
385
386}