package armadocdownloader; import org.jetbrains.annotations.NotNull; import org.jsoup.Jsoup; import org.jsoup.nodes.Attribute; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.select.Elements; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.util.Iterator; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicInteger; /** * @author Kayler * @since 09/20/2017 */ public class ArmaDocumentationAsyncFormattingOperations { private static final String getDocNameCSSCode = "#firstHeading"; private static final String getDocumentationDescriptionCSS = "._description"; private static final LinkedBlockingQueue<Job> jobs = new LinkedBlockingQueue<>(); private static final AtomicInteger endJobsCount = new AtomicInteger(0); private static final Job END_COMMANDS = new Job(); private static final Job END_FUNCTIONS = new Job(); static { Runnable r = () -> { while (endJobsCount.get() < 2) { try { Job take = jobs.take(); if (take == END_COMMANDS) { endJobsCount.incrementAndGet(); END_COMMANDS.retriever.allDoneFormatting(); System.out.println("No more commands added to format queue."); continue; } if (take == END_FUNCTIONS) { endJobsCount.incrementAndGet(); END_FUNCTIONS.retriever.allDoneFormatting(); System.out.println("No more BIS functions added to format queue."); continue; } doFormatAndSave(take.srcFile, take.destFile, take.linkType); } catch (Exception e) { e.printStackTrace(System.out); } } }; Thread t = new Thread(r); t.setName("IntelliJ Formatted Documentation Saver Thread 1"); t.setDaemon(false); t.start(); Thread t2 = new Thread(r); t2.setName("IntelliJ Formatted Documentation Saver Thread 2"); t2.setDaemon(false); t2.start(); } /** * Invoke when you have designated all commands have been queued for formatting */ public static void noMoreCommandsToFormat(@NotNull Arma3CommandsDocumentationRetriever retriever) { jobs.add(END_COMMANDS.bindRetriever(retriever)); } public static void noMoreFunctionsToFormat(@NotNull Arma3FunctionsDocumentationRetriever retriever) { jobs.add(END_FUNCTIONS.bindRetriever(retriever)); } public static void formatAndSaveAsync(@NotNull File srcFile, @NotNull File destFile, @NotNull PsiElementLinkType linkType) { jobs.add(new Job(srcFile, destFile, linkType)); } private static void doFormatAndSave(@NotNull File srcFile, @NotNull File destFile, @NotNull PsiElementLinkType linkType) { final Document document; String description, docName; try { document = Jsoup.parse(srcFile, null, Arma3DocumentationDownloader.BASE_WIKI_URL); document.setBaseUri(Arma3DocumentationDownloader.BASE_WIKI_URL); document.outputSettings().prettyPrint(false); HTMLUtil.convertURLsToBase(document); } catch (IOException e) { e.printStackTrace(); return; } docName = document.select(getDocNameCSSCode).get(0).text().replaceAll("\\s", "_"); Elements codeEles = document.select("code, pre"); for (Element code : codeEles) { for (Attribute attribute : code.attributes()) { code.removeAttr(attribute.getKey()); } } //make non-function and non-command links green Elements aEles = document.select("a"); for (Element aEle : aEles) { boolean setColorToGreen = true; for (PsiElementLinkType type : PsiElementLinkType.allTypes) { if (type.linkNames.contains(aEle.text())) { aEle.attr("href", "psi_element://" + type.type + ":" + aEle.text()); setColorToGreen = false; break; } } if (setColorToGreen) { aEle.attr("style", "color:008800;"); } } StringBuilder allNotesBuilder = new StringBuilder(); { //this needs to come before we trim down the description, otherwise it will get deleted /*As of September 21, 2017, the wiki's notes are formatted as follows: * <dl class="command_description"> <!-- This dl element can repeat multiple times--> * <!-- START_NOTE--> * <dd class="notedate">note's date</dd> * <dt class="note">author</dd> * <dd class="note">the actual note body</dd> * <!-- END_NOTE--> * </dl> * * Between HTML comment "START_NOTE" "END_NOTE" defines a note's date, author, and body. * Each of those elements are repeated for every note. * */ allNotesBuilder.append("<h3>Notes</h3>"); int noteCount = 0; for (Element noteCluster : document.getElementsByClass("command_description")) { int notePart = 0;//when reached 2, that defines a whole note's date, author, and body StringBuilder singleNoteBuilder = new StringBuilder(); for (Element noteClusterChild : noteCluster.children()) { if (noteClusterChild.hasClass("note") || noteClusterChild.hasClass("notedate")) { String elementHtml = fixTags(noteClusterChild.html()); singleNoteBuilder.append("<div>"); singleNoteBuilder.append(notePart == 0 ? "<b>Date: </b>" : notePart == 1 ? "<b>Author: </b>" : ""); singleNoteBuilder.append(elementHtml); singleNoteBuilder.append("</div>"); if (notePart < 2) { notePart++; } else { allNotesBuilder.append("<div style=\"border:2px solid #cacaca;padding:4px;\""); allNotesBuilder.append(singleNoteBuilder); allNotesBuilder.append("</div><br/>"); singleNoteBuilder = new StringBuilder(); notePart = 0; noteCount++; } } } } if (noteCount == 0) { allNotesBuilder.append("No notes."); } } Elements descriptionElements = document.select(getDocumentationDescriptionCSS); Elements descEleChild; try { descEleChild = descriptionElements.get(0).children(); } catch (Exception e) { System.out.println("couldn't find description element for: " + srcFile); e.printStackTrace(); return; } Iterator<Element> descEleChildIter = descEleChild.iterator(); int childIndex = -1; while (descEleChildIter.hasNext()) { childIndex++; Element e = descEleChildIter.next(); if (e.tagName().equalsIgnoreCase("h3") || e.tagName().equalsIgnoreCase("dl") || e.tagName().equalsIgnoreCase("span")) { if (e.tagName().equalsIgnoreCase("h3")) { e.html(e.text()); } if (e.tagName().equalsIgnoreCase("h3") && childIndex == descEleChild.size() - 2) { //-2 because the last element isn't actually -1 for some reason e.remove(); } continue; } else { e.remove(); } } cleanAttributes(document); description = fixTags(descriptionElements.html()); System.out.println("Saving " + linkType.type + " doc: " + docName); try { PrintWriter pw = new PrintWriter(destFile); pw.println(description); pw.flush(); pw.println(allNotesBuilder.toString()); pw.flush(); pw.close(); } catch (Exception e) { e.printStackTrace(); } } private static void cleanAttributes(@NotNull Document d) { Elements allEles = d.select("*"); for (Element code : allEles) { code.removeAttr("class"); code.removeAttr("title"); code.removeAttr("id"); } } private static String fixTags(@NotNull String replaceText) { replaceText = replaceTag(replaceText, "code", "pre", ""); replaceText = replaceText.replaceAll("/a><a", "/a> <a"); //in intellij, pre behaves weird when it has a border. so nest it in a parent div that has the border String newPreTag = "<div style='border: 1px dashed #A9A9A9;margin:5px 0px;padding:4px;'><pre>"; replaceText = replaceText.replaceAll("<pre[ ]*>", newPreTag); //must come first or 2 borders will be created replaceText = replaceText.replaceAll("</pre>", "</pre></div>"); return replaceText; } private static String replaceTag(@NotNull String replaceText, @NotNull String tagName, @NotNull String newTagName, @NotNull String attributes) { replaceText = replaceText.replaceAll("<" + tagName + ">", "<" + newTagName + " " + attributes + " >"); replaceText = replaceText.replaceAll("</" + tagName + ">", "</" + newTagName + ">"); return replaceText; } private static class Job { private File srcFile; private File destFile; private PsiElementLinkType linkType; private WikiDocumentationRetriever retriever; public Job() { } public Job(@NotNull File srcFile, @NotNull File destFile, @NotNull PsiElementLinkType linkType) { this.srcFile = srcFile; this.destFile = destFile; this.linkType = linkType; } @NotNull Job bindRetriever(@NotNull WikiDocumentationRetriever retriever) { this.retriever = retriever; return this; } } }