From 92d46ba6144c430498f4f1d0edd7a1564ffcf792 Mon Sep 17 00:00:00 2001 From: Pete Date: Wed, 8 Nov 2023 17:21:27 +0000 Subject: [PATCH] Improve markdown parsing Use commonmark extension to remove yaml frontmatter, and insert it at the end of the doc. Also add name and description (if available) as headings. (This will require the latest QuPath v0.5.0 snapshot to add the yaml support) --- build.gradle | 4 +- .../ext/wsinfer/models/WSInferModel.java | 24 ++++++- .../ext/wsinfer/ui/WSInferController.java | 72 ++++++++++++++++--- .../ext/wsinfer/ui/wsinfer_control.fxml | 5 -- 4 files changed, 86 insertions(+), 19 deletions(-) diff --git a/build.gradle b/build.gradle index e644da4..a774493 100644 --- a/build.gradle +++ b/build.gradle @@ -37,8 +37,8 @@ dependencies { // Main QuPath user interface jar. // Automatically includes other QuPath jars as subdependencies. implementation "io.github.qupath:qupath-gui-fx:${qupathVersion}" - implementation 'io.github.qupath:qupath-fxtras:0.1.0' - implementation 'org.commonmark:commonmark:0.21.0' + implementation libs.qupath.fxtras + implementation libs.bundles.markdown // For logging - the version comes from QuPath's version catalog at // https://github.com/qupath/qupath/blob/main/gradle/libs.versions.toml diff --git a/src/main/java/qupath/ext/wsinfer/models/WSInferModel.java b/src/main/java/qupath/ext/wsinfer/models/WSInferModel.java index e7239c8..10c3f5a 100644 --- a/src/main/java/qupath/ext/wsinfer/models/WSInferModel.java +++ b/src/main/java/qupath/ext/wsinfer/models/WSInferModel.java @@ -54,6 +54,14 @@ public String getName() { return hfRepoId; } + /** + * Get a description, if available (may be null). + * @return + */ + public String getDescription() { + return description; + } + /** * Get the configuration. Note that this may be null. * @return the model configuration, or null. @@ -69,8 +77,20 @@ public WSInferModelConfiguration getConfiguration() { * Remove the cached model files. */ public synchronized void removeCache() { - getTorchScriptFile().delete(); - getConfigFile().delete(); + removeIfFound(getTorchScriptFile(), getConfigFile(), getReadMeFile()); + } + + private static void removeIfFound(File... files) { + for (var file : files) { + if (file != null && file.isFile()) { + try { + logger.debug("Deleting file {}", file); + file.delete(); + } catch (Exception e) { + logger.error("Unable to delete file {}", file, e); + } + } + } } /** diff --git a/src/main/java/qupath/ext/wsinfer/ui/WSInferController.java b/src/main/java/qupath/ext/wsinfer/ui/WSInferController.java index c453cc0..3520ed9 100644 --- a/src/main/java/qupath/ext/wsinfer/ui/WSInferController.java +++ b/src/main/java/qupath/ext/wsinfer/ui/WSInferController.java @@ -42,6 +42,8 @@ import javafx.scene.web.WebView; import javafx.stage.Stage; import javafx.util.StringConverter; +import org.commonmark.ext.front.matter.YamlFrontMatterExtension; +import org.commonmark.ext.front.matter.YamlFrontMatterVisitor; import org.commonmark.parser.Parser; import org.commonmark.renderer.html.HtmlRenderer; import org.controlsfx.control.PopOver; @@ -75,7 +77,7 @@ import java.io.File; import java.io.IOException; import java.nio.file.Files; -import java.nio.file.Path; +import java.util.Arrays; import java.util.Collection; import java.util.Objects; import java.util.ResourceBundle; @@ -127,8 +129,9 @@ public class WSInferController { private TextField tfModelDirectory; @FXML private TextField localModelDirectory; - @FXML - private PopOver infoPopover; + + private WebView infoWebView = WebViews.create(true); + private PopOver infoPopover = new PopOver(infoWebView); private final static ResourceBundle resources = ResourceBundle.getBundle("qupath.ext.wsinfer.ui.strings"); @@ -187,11 +190,15 @@ private void configureModelChoices() { modelChoiceBox.getSelectionModel().selectedItemProperty().addListener( (v, o, n) -> { downloadButton.setDisable((n == null) || n.isValid()); - infoButton.setDisable((n == null) || (!n.isValid()) || n instanceof WSInferModelLocal); + infoButton.setDisable((n == null) || (!n.isValid()) || !checkFileExists(n.getReadMeFile())); infoPopover.hide(); }); } + private static boolean checkFileExists(File file) { + return file != null && file.isFile(); + } + private void configureAvailableDevices() { var available = PytorchManager.getAvailableDevices(); deviceChoices.getItems().setAll(available); @@ -393,14 +400,59 @@ public void showInfo() throws IOException { return; } WSInferModel model = modelChoiceBox.getSelectionModel().getSelectedItem(); - Path mdFile = model.getReadMeFile().toPath(); - var doc = Parser.builder().build().parse(Files.readString(mdFile)); - WebView webView = WebViews.create(true); - webView.getEngine().loadContent(HtmlRenderer.builder().build().render(doc)); - infoPopover.setContentNode(webView); - infoPopover.show(infoButton); + var file = model.getReadMeFile(); + if (!checkFileExists(file)) { + logger.warn("ReadMe file not available: {}", file); + return; + } + try { + var markdown = Files.readString(file.toPath()); + + // Parse the initial markdown only, to extract any YAML front matter + var parser = Parser.builder() + .extensions( + Arrays.asList(YamlFrontMatterExtension.create()) + ).build(); + var doc = parser.parse(markdown); + var visitor = new YamlFrontMatterVisitor(); + doc.accept(visitor); + var metadata = visitor.getData(); + + // If we have YAML metadata, remove from the start (since it renders weirdly) and append at the end + if (!metadata.isEmpty()) { + doc.getFirstChild().unlink(); + var sb = new StringBuilder(); + sb.append("----\n\n"); + sb.append("### Metadata\n\n"); + for (var entry : metadata.entrySet()) { + sb.append("\n* **").append(entry.getKey()).append("**: ").append(entry.getValue()); + } + doc.appendChild(parser.parse(sb.toString())); + } + + // If the markdown doesn't start with a title, pre-pending the model title & description (if available) + if (!markdown.startsWith("#")) { + var sb = new StringBuilder(); + sb.append("## ").append(model.getName()).append("\n\n"); + var description = model.getDescription(); + if (description != null && !description.isEmpty()) { + sb.append("_").append(description).append("_").append("\n\n"); + } + sb.append("----\n\n"); + doc.prependChild(parser.parse(sb.toString())); + } + + infoWebView.getEngine().loadContent( + HtmlRenderer.builder().build().render(doc)); + infoPopover.show(infoButton); + } catch (IOException e) { + logger.error("Error parsing readme file", e); + } } + + + @FXML private void selectAllAnnotations() { Commands.selectObjectsByClass(imageDataProperty.get(), PathAnnotationObject.class); diff --git a/src/main/resources/qupath/ext/wsinfer/ui/wsinfer_control.fxml b/src/main/resources/qupath/ext/wsinfer/ui/wsinfer_control.fxml index a4e09b1..461bc9d 100644 --- a/src/main/resources/qupath/ext/wsinfer/ui/wsinfer_control.fxml +++ b/src/main/resources/qupath/ext/wsinfer/ui/wsinfer_control.fxml @@ -19,7 +19,6 @@ - @@ -50,10 +49,6 @@ - - - -