diff --git a/src/main/java/com/knowledgepixels/nanodash/Utils.java b/src/main/java/com/knowledgepixels/nanodash/Utils.java index bd176f29..333ee2f5 100644 --- a/src/main/java/com/knowledgepixels/nanodash/Utils.java +++ b/src/main/java/com/knowledgepixels/nanodash/Utils.java @@ -21,10 +21,12 @@ import org.eclipse.rdf4j.model.IRI; import org.eclipse.rdf4j.model.Literal; import org.eclipse.rdf4j.model.Statement; +import org.eclipse.rdf4j.model.Value; import org.eclipse.rdf4j.model.ValueFactory; import org.eclipse.rdf4j.model.impl.SimpleValueFactory; import org.eclipse.rdf4j.model.util.Literals; import org.eclipse.rdf4j.model.vocabulary.FOAF; +import org.eclipse.rdf4j.model.vocabulary.RDF; import org.eclipse.rdf4j.model.vocabulary.XSD; import org.nanopub.Nanopub; import org.nanopub.NanopubUtils; @@ -673,6 +675,10 @@ public static String sanitizeSvg(String rawSvg) { /** * Checks if a given string is likely to be HTML content. * + *
This is a heuristic, needed wherever the RDF datatype is not available (query + * results arrive as flat strings). Where an actual literal is at hand, prefer + * {@link #isHtmlLiteral(Value)}, which relies on the declared datatype instead. + * * @param value the string to check * @return true if the given string is HTML content, false otherwise */ @@ -680,6 +686,17 @@ public static boolean looksLikeHtml(String value) { return LEADING_TAG.matcher(value).find(); } + /** + * Checks whether a value is a literal explicitly tagged with the {@code rdf:HTML} + * datatype, i.e. whose content is meant to be rendered as HTML. + * + * @param value the value to check + * @return true if the value is an rdf:HTML literal, false otherwise + */ + public static boolean isHtmlLiteral(Value value) { + return value instanceof Literal literal && RDF.HTML.equals(literal.getDatatype()); + } + public static boolean isDate(String value) { return isDateLiteral(value) || isDateTimeLiteral(value); } diff --git a/src/main/java/com/knowledgepixels/nanodash/component/LiteralItem.html b/src/main/java/com/knowledgepixels/nanodash/component/LiteralItem.html index 79082010..e7f6e1b7 100644 --- a/src/main/java/com/knowledgepixels/nanodash/component/LiteralItem.html +++ b/src/main/java/com/knowledgepixels/nanodash/component/LiteralItem.html @@ -7,7 +7,7 @@
The datatype decides: {@code rdf:HTML} on the literal itself, or declared by the + * template for this placeholder, means HTML. Only when the template declares no + * datatype at all do we fall back to the pattern heuristic, which covers nanopubs + * published before HTML content was tagged, and those made without a template + * (see issue #378). + * + * @param literal the literal to be displayed + * @return true if the content is to be rendered as HTML + */ + private boolean renderAsHtml(Literal literal) { + if (Utils.isHtmlLiteral(literal)) return true; + // A language-tagged literal is an rdf:langString, so a datatype the template + // declares alongside the tag does not apply to it. + IRI declaredDatatype = literal.getLanguage().isPresent() ? null : template.getDatatype(iri); + if (declaredDatatype != null) return RDF.HTML.equals(declaredDatatype); + return Utils.looksLikeHtml(literal.stringValue()); + } + /** * Validator class for validating the input. */ diff --git a/src/test/java/com/knowledgepixels/nanodash/UtilsTest.java b/src/test/java/com/knowledgepixels/nanodash/UtilsTest.java index 7c4733f4..e206474d 100644 --- a/src/test/java/com/knowledgepixels/nanodash/UtilsTest.java +++ b/src/test/java/com/knowledgepixels/nanodash/UtilsTest.java @@ -1,6 +1,9 @@ package com.knowledgepixels.nanodash; -import com.knowledgepixels.nanodash.utils.TestUtils; +import java.util.List; +import java.util.Map; +import java.util.Set; + import org.apache.wicket.markup.html.link.ExternalLink; import org.apache.wicket.request.mapper.parameter.PageParameters; import org.apache.wicket.util.tester.WicketTester; @@ -8,22 +11,29 @@ import org.eclipse.rdf4j.model.Literal; import org.eclipse.rdf4j.model.Statement; import org.eclipse.rdf4j.model.util.Values; +import static org.eclipse.rdf4j.model.util.Values.iri; +import static org.eclipse.rdf4j.model.util.Values.literal; import org.eclipse.rdf4j.model.vocabulary.FOAF; +import org.eclipse.rdf4j.model.vocabulary.RDF; import org.eclipse.rdf4j.model.vocabulary.XSD; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; import org.mockito.MockedStatic; -import org.nanopub.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; +import org.nanopub.MalformedNanopubException; +import org.nanopub.Nanopub; +import org.nanopub.NanopubAlreadyFinalizedException; +import org.nanopub.NanopubCreator; +import org.nanopub.NanopubUtils; import org.nanopub.vocabulary.FIP; import org.nanopub.vocabulary.NPX; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import static org.eclipse.rdf4j.model.util.Values.iri; -import static org.eclipse.rdf4j.model.util.Values.literal; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; +import com.knowledgepixels.nanodash.utils.TestUtils; class UtilsTest { @@ -137,6 +147,22 @@ void sanitizeHtmlPolicyPreservesAllowedProtocols() { assertEquals("Email", sanitizedHtml); } + @Test + void isHtmlLiteralRecognizesRdfHtmlDatatype() { + assertTrue(Utils.isHtmlLiteral(literal("
Hello
", RDF.HTML))); + assertTrue(Utils.isHtmlLiteral(literal("no tags at all", RDF.HTML)), + "the datatype decides, not the content"); + } + + @Test + void isHtmlLiteralRejectsOtherValues() { + assertFalse(Utils.isHtmlLiteral(literal("Hello
")), "plain string literal"); + assertFalse(Utils.isHtmlLiteral(literal("Hello
", "en")), "language-tagged literal"); + assertFalse(Utils.isHtmlLiteral(literal("42", XSD.INTEGER)), "other datatype"); + assertFalse(Utils.isHtmlLiteral(iri("https://example.org/thing")), "IRI"); + assertFalse(Utils.isHtmlLiteral(null), "null"); + } + @Test void sanitizeSvgKeepsStaticSvgSubset() { String rawSvg = ""; String sanitized = Utils.sanitizeSvg(rawSvg); - for (String kept : new String[] {"