Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions src/main/java/org/jabref/htmltonode/rich/RichTextRenderer.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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<Block> blocks) {
for (Block block : blocks) {
switch (block) {
Expand Down
21 changes: 21 additions & 0 deletions src/test/java/org/jabref/htmltonode/RichTextRendererTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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("a<img src=\"data:image/png;base64,"
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==\">b");

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 <a href=\"https://doi.org/10.1/x\">10.1/x</a>");

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("""
Expand Down