diff --git a/src/main/java/com/knowledgepixels/nanodash/component/SourceNanopub.html b/src/main/java/com/knowledgepixels/nanodash/component/SourceNanopub.html index 2200af99..a96e68b8 100644 --- a/src/main/java/com/knowledgepixels/nanodash/component/SourceNanopub.html +++ b/src/main/java/com/knowledgepixels/nanodash/component/SourceNanopub.html @@ -3,7 +3,7 @@
+ * A page returns one only when it is that resource's own page -- the one its IRI + * resolves to. A page that merely links to a nanopublication is not announcing itself as + * that nanopublication, and a listing that links to many is announcing itself as none of + * them (issue #716). + * + * @return the IRI to be cited as, or null + */ + protected String getCiteAsIri() { + return null; + } + /** * Answers a client that asked for RDF in its {@code Accept} header with a 303 to the * download page in the matching format, and lets everyone else have the HTML (issue @@ -420,6 +434,31 @@ private void renderRdfLinks(IHeaderResponse response) { response.render(StringHeaderItem.forString("\n")); } + // The vocabulary the cite-as relation is a term of, scoped to the one link that uses it: + // with it in scope every token of that link's rel resolves against it, which is why the + // link carries the relation and nothing else (issues #633, #716). + private static final String LINK_RELATIONS_VOCAB = "https://www.w3.org/ns/iana/link-relations/relation#"; + + /** + * Renders the {@code cite-as} link (RFC + * 8574) that says which IRI this page is to be cited as, for pages that are a resource's + * own page. Annotation tools read it to anchor to the resource rather than to whichever URL + * the reader arrived by (issue #633). + *
+ * The {@code vocab} attribute lets an RDFa parser resolve the relation, as dokieli's does. + * Rendered into the head rather than onto the visible IRI link: a {@code rel} is a list of + * tokens and the visible link already carries {@code noopener noreferrer}, which the vocab + * would turn into link-relation triples of their own. + * + * @param response the header response to render into + */ + private void renderCiteAs(IHeaderResponse response) { + String citeAsIri = getCiteAsIri(); + if (citeAsIri == null || citeAsIri.isBlank()) return; + response.render(MetaDataHeaderItem.forLinkTag("cite-as", citeAsIri) + .addTagAttribute("vocab", LINK_RELATIONS_VOCAB)); + } + /** * Renders the description, canonical URL, Open Graph and Twitter card tags that * search engines and link previews read (issue #704). @@ -470,6 +509,7 @@ public void renderHead(IHeaderResponse response) { super.renderHead(response); renderPageMetadata(response); renderRdfLinks(response); + renderCiteAs(response); response.render(CssHeaderItem.forUrl(getStyleSheetUrl())); response.render(JavaScriptHeaderItem.forReference(getApplication().getJavaScriptLibrarySettings().getJQueryReference())); response.render(JavaScriptReferenceHeaderItem.forReference(nanodashJs)); diff --git a/src/test/java/com/knowledgepixels/nanodash/page/CiteAsLinkTest.java b/src/test/java/com/knowledgepixels/nanodash/page/CiteAsLinkTest.java new file mode 100644 index 00000000..117edcad --- /dev/null +++ b/src/test/java/com/knowledgepixels/nanodash/page/CiteAsLinkTest.java @@ -0,0 +1,76 @@ +package com.knowledgepixels.nanodash.page; + +import com.knowledgepixels.nanodash.WicketApplication; +import org.apache.wicket.util.tester.WicketTester; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * The {@code cite-as} link of RFC 8574 says which IRI the page being read is to be cited as, + * so it belongs on a resource's own page and nowhere else: a page that merely links to + * nanopublications is not announcing itself as one of them (issues #633, #716). + */ +class CiteAsLinkTest { + + private WicketTester tester; + + @BeforeEach + void setUp() { + tester = new WicketTester(new WicketApplication()); + } + + private String render(Class extends NanodashPage> pageClass) { + tester.startPage(pageClass); + return tester.getLastResponse().getDocument(); + } + + @Test + void aResourcesOwnPageSaysWhatToCiteItAs() { + String document = render(CiteAsSubjectPage.class); + assertTrue(document.contains("rel=\"cite-as\""), document); + assertTrue(document.contains("href=\"" + CiteAsSubjectPage.SUBJECT_IRI + "\""), document); + } + + // Without the vocabulary in scope an RDFa parser has no IRI to resolve the relation to, + // and dokieli -- the reason the link is here at all -- reads it through one. + @Test + void theRelationIsResolvableByAnRdfaParser() { + String document = render(CiteAsSubjectPage.class); + assertTrue(document.contains("vocab=\"https://www.w3.org/ns/iana/link-relations/relation#\""), document); + } + + // The link carries the relation and nothing else. Every token of a rel resolves against + // the vocabulary in scope, so a noopener alongside it would assert a link relation of its + // own about the page -- which is why this is a link of its own rather than an attribute on + // the visible IRI link, where noopener and noreferrer already sit. + @Test + void theCiteAsLinkCarriesNoOtherRelation() { + String document = render(CiteAsSubjectPage.class); + int relIndex = document.indexOf("rel=\"cite-as\""); + assertTrue(relIndex > -1, document); + int lineStart = document.lastIndexOf("<", relIndex); + int lineEnd = document.indexOf(">", relIndex); + String tag = document.substring(lineStart, lineEnd + 1); + assertFalse(tag.contains("noopener"), tag); + assertFalse(tag.contains("noreferrer"), tag); + } + + // A page with no subject of its own claims nothing, which is the case every listing and + // every page that merely links to a nanopublication falls into. + @Test + void aPageThatIsNotAResourcesOwnPageClaimsNothing() { + String document = render(ErrorPage.class); + assertFalse(document.contains("cite-as"), document); + } + + @Test + void exactlyOneCiteAsIsClaimed() { + String document = render(CiteAsSubjectPage.class); + assertEquals(1, document.split("rel=\"cite-as\"", -1).length - 1, document); + } + +} diff --git a/src/test/java/com/knowledgepixels/nanodash/page/CiteAsSubjectPage.html b/src/test/java/com/knowledgepixels/nanodash/page/CiteAsSubjectPage.html new file mode 100644 index 00000000..7c461a15 --- /dev/null +++ b/src/test/java/com/knowledgepixels/nanodash/page/CiteAsSubjectPage.html @@ -0,0 +1,13 @@ + + +
+cite-as test page