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}