From 3b1096b73742c6a332869770c8ddde84775c3da9 Mon Sep 17 00:00:00 2001 From: Oliver Kopp Date: Wed, 8 Jul 2026 01:35:45 +0200 Subject: [PATCH] Extract link resolution into hrefAt and test image clicks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The click handler resolves a link via getStyleAttributeMap(...).get(HREF). Node segments (embedded images, tables) have no character attributes, so the model returns a null attribute map at their position — clicking a book-cover image therefore lands on a null map. The inline guard added in #7 already tolerates this; extracting it into a public hrefAt(model, position) helper makes the guard unit-testable without the JavaFX toolkit and gives callers that install their own click handler a supported way to resolve the target at a position. Adds headless regression tests: hrefAt returns null (no throw) at an inline image's position, and still resolves the target for a real link run. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../htmltonode/rich/RichTextRenderer.java | 15 +++++++++---- .../htmltonode/RichTextRendererTest.java | 21 +++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/jabref/htmltonode/rich/RichTextRenderer.java b/src/main/java/org/jabref/htmltonode/rich/RichTextRenderer.java index 03328fc..dd0c1a4 100644 --- a/src/main/java/org/jabref/htmltonode/rich/RichTextRenderer.java +++ b/src/main/java/org/jabref/htmltonode/rich/RichTextRenderer.java @@ -93,10 +93,7 @@ public static void configure(RichTextArea area, HtmlRenderOptions options) { if (event.getButton() == MouseButton.PRIMARY && event.isStillSincePress()) { TextPos position = area.getTextPosition(event.getScreenX(), event.getScreenY()); if (position != null) { - // Node segments (embedded images, tables) carry no character attributes, so the - // model returns null there — only text runs can hold an HREF. - StyleAttributeMap attributes = area.getModel().getStyleAttributeMap(null, position); - String href = attributes != null ? attributes.get(HREF) : null; + String href = hrefAt(area.getModel(), position); if (href != null) { options.linkHandler().accept(href); } @@ -105,6 +102,16 @@ public static void configure(RichTextArea area, HtmlRenderOptions options) { }); } + /// Returns the link target at `position`, or `null` if there is none. + /// + /// Node segments (embedded images, tables) carry no character attributes, so the model + /// returns a null attribute map there — only text runs can hold an [#HREF]. Clicking an + /// image therefore lands on a null map, which this method tolerates. + public static @Nullable String hrefAt(StyledTextModel model, TextPos position) { + StyleAttributeMap attributes = model.getStyleAttributeMap(null, position); + return attributes != null ? attributes.get(HREF) : null; + } + private void appendBlocks(List blocks) { for (Block block : blocks) { switch (block) { diff --git a/src/test/java/org/jabref/htmltonode/RichTextRendererTest.java b/src/test/java/org/jabref/htmltonode/RichTextRendererTest.java index 8cc70a9..b75c79d 100644 --- a/src/test/java/org/jabref/htmltonode/RichTextRendererTest.java +++ b/src/test/java/org/jabref/htmltonode/RichTextRendererTest.java @@ -142,6 +142,27 @@ void blockImageBecomesEmbeddedParagraph() { assertTrue(model.size() >= 2, "expected an embedded image paragraph before the text"); } + @Test + void hrefAtImagePositionIsNullAndDoesNotThrow() { + // Clicking a book-cover / inline image resolves to a node segment, for which the model + // returns a null attribute map. hrefAt must tolerate that (regression for an NPE that + // dereferenced getStyleAttributeMap(...) on image clicks); offset 1 is the image node. + StyledTextModel model = model("ab"); + + assertEquals("a b", model.getPlainText(0)); + assertNull(attributesAt(model, 0, 1), "image node segment carries no character attribute map"); + assertNull(RichTextRenderer.hrefAt(model, TextPos.ofLeading(0, 1))); + } + + @Test + void hrefAtLinkPositionReturnsTarget() { + StyledTextModel model = model("see 10.1/x"); + + assertEquals("https://doi.org/10.1/x", RichTextRenderer.hrefAt(model, TextPos.ofLeading(0, 5))); + assertNull(RichTextRenderer.hrefAt(model, TextPos.ofLeading(0, 1))); + } + @Test void cslEntryRendersAsStyledCitation() { StyledTextModel model = model("""