Proposal: new API in rendering to give "wanted links" a proper text alternative

Hello!

Context

XWIKI-19383: when a link points to a page that does not exist, XWiki renders a “wanted link”. A wanted link is shown with its label, a ? marker, and a href pointing at the creation URL:

<span class="wikicreatelink"><a href="...create..."><span class="wikigeneratedlinkcontent">Label</span></a></span>

Nothing in there says that following the link creates a page rather than opening one. A sighted user has the ? badge in the primary color to highlight this difference. A screen reader user gets a plain link named “Label”, indistinguishable from a working one.

Proposed implementations are in PR xwiki-rendering#256 for the API, and PR xwiki-platform#2104 for their use with translations.

Proposal

Add a component role to xwiki-rendering-api that names the resource a wanted link would create, and have the XHTML link renderer expose what it returns as the text alternative of the link. The role would be org.xwiki.rendering.renderer.reference.link.WantedLinkTitleGenerator:

@Role
@Unstable
public interface WantedLinkTitleGenerator
{
    /**
     * @param reference the reference of the resource the wanted link points to
     * @return the text alternative describing the resource the wanted link would create, as plain text, or
     *         {@code null} to render the link without one
     */
    String generateWantedLinkTitle(ResourceReference reference);
}
  • Contract. One method, one argument, and the reference as the only input: an implementation would be a pure naming function, deciding nothing about whether the link is wanted in the first place. The returned string would be plain text, escaped by the renderer, and a null return would leave the link without a text alternative.
  • **Lookup.**When the target does not exist DocumentXHTMLLinkTypeRenderer.beginInternalLink would ask the component manager for the implementation registered under a type hint: doc, page or space. When there is no implementation for that type, it would fallback on a no-hint default implementation. A lookup that fails would log a warning and fallback similarly. An implementation registered under a hint that is not used would simply never be called.
  • Reach. doc, page and space are the only references that can produce a wanted link at all: only links to a page (doc:, page:) or to a space (space:) are checked against the wiki to see whether their target exists. A link to an attachment, a mail address or a URL is rendered by another class.
  • Default. DefaultWantedLinkTitleGenerator would be registered without a hint and return Create resource: <reference>. It would be untranslated on purpose, since the rendering module has no access to the localization. It’s the same pattern DefaultPageAttachmentURILabelGenerator and DataURILabelGenerator already follow for labels, the first returning the raw reference and the second a hardcoded English Data URI image.

In XS, the text alternatives a user actually gets would then come from implementations registered in xwiki-platform-rendering-xwiki, all using the ContextualLocalizationManager (page/doc, space and a tranlated default specific to xwiki-platform).

Opinion

+1 from me for adding the API as described above, because:

  • Nothing in the current markup tells a screen reader user that the link creates a page instead of opening one, and there is no way to add that from the outside: the wanted link is built by the renderer.
  • The wording would have to come from outside xwiki-rendering to be translated, and to differ per kind of resource, so a component role seems like the shape that fits. It is also the shape already used right next to it because computeLabel resolves a URILabelGenerator using the type of the link reference.
  • The complexity would stay limited: one role, one default implementation, and the three implementations on platform.

Conclusion

This new role would become a public API, we need to agree on it so that we can avoid needing to revert it when it’s unstable (or getting stuck with a poor choice after that). There’s at least two things I’d like to settle:

  • Naming Are WantedLinkTitleGenerator and generateWantedLinkTitle(ResourceReference) proper names, and is the reference a correct parameter?
  • Architecture Does a component role resolved by the type of the reference fit into the architecture, or would another way to do things hold better against time?

Anything else you would want discussed before such a role is validated is welcome to the discussion of course. I’ll close this topic in a week if it doesn’t spur a larger discussion.

Thank you for your interest in the topic! I’m looking forward to your feedback :slight_smile:
Lucas C.

I think the pragmatic choice here is to make this role internal. Using internal roles across different XS projects (xwiki-commons, xwiki-rendering, xwiki-platform) is a very common choice. I don’t see why this role would need to be a public API.

Thanks Michael, that makes sense to me, +1 for making it internal.
I had framed this as a public API because the implementations live in xwiki-platform while the role and the caller live in xwiki-rendering, but you’re right that crossing a project boundary doesn’t require the role to be public, that’s exactly what the internal concept is for. I don’t have any use case outside XS so there’s no point in making it public. So I’ll move the role to org.xwiki.rendering.internal.renderer.reference.link and drop @Unstable.

That also lowers the stakes of the proposal. It was important because the role would have been frozen as public API. I’m still interested in opinions on these questions but as a design preference now rather than as a commitment.

I think @tmortagne suggested already some time ago that we should move the localization APIs to xwiki-rendering such that we can use them there. This would probably make it much easier to directly implement this feature in xwiki-rendering and eliminate the need for such a dedicated component. However, this is a bigger change and I guess introducing this component similar to the existing examples makes sense.

Side note: I’d understand moving localization APIs to xwiki-commons, but I don’t understand what it has to do with xwiki-rendering only.

The reason is that the core Translation interface renders a translation to an org.xwiki.rendering.block.Block so unless we move the core rendering APIs to xwiki-commons, too, translations can only be moved to xwiki-rendering and not to xwiki-commons.

ok I see, I’d forgotten that the Translation API was linked to Block. I’d still find it weird to not have some generic localization module in commons (without the notion of Block). For ex the concept of TranslationBundle should be completely generic. And then have a rendering localization module on top of the more generic one, for rendering translations. There could be strings to translate in xwiki-commons modules. And then, ofc, we’ll still need a localization module in xwiki-platform too, on top of the other two (e.g. for storing translations in wiki pages as xobjects, for setting a language in the context, etc).

WDYT?

Thx