Problem/Motivation

See #2418017-65: Implement autocomplete UI for the link widget for a more practical explanation that is necessary.

Jumplinks
Currently, it is impossible to create jumplinks: menu links that render as
<a href="#fragment_to_jump_to">Amazing Llama Tricks!</a>

This is because user-path: maps to <front> (i.e. http://drupal.com/), hence user-path:#kitten renders to /#kittens (i.e. http://drupal.com/#kittens). In other words: currently we can only create jump links that jump to a fragment on a specific URL.
But we also want to be able to create user-path:-scheme URIs that map to <none>, so that we can create jump links that jump to a fragment on whatever the current page is, i.e. URIs that render to #kittens, not /#kittens.

To do that, we can assign additional meaning to a leading slash: the leading slash would mean "relative to the Drupal root", no leading slash would mean "relative to the current page".

So, instead of user-path:<path>, we'd have user-path:/<path>. Which then allows us to have user-path:#<fragment>, allowing us to create page-relative jumplinks.

Note that none of this needs to be exposed to the user! The user would still:

  1. start to type a title to autocomplete to an entity: URI
  2. type an path that begins with a slash (/) to type a user-path: URI (note how this also maps precisely to what is being proposed here)
  3. NEW (kind of, was already planned, but is currently impossible): type a fragment that beings with a hash (#) to type a user-path: URI. You could do this in HEAD, but it'd always result in a fragment to the front page. With this proposed change it'd allow the user to create an page-relative jumplink.
Consistency with Symfony
Currently, we don't store paths in user-path: URIs in the same way as Symfony does (with a leading slash). This would bring additional consistency.

Proposed resolution

See above.

Remaining tasks

Needs review/discussion.

User interface changes

None.

API changes

user-path:<path> -> user-path:/<path>

Comments

webchick’s picture

But... why? :( We don't do that with either base: nor entity:

wim leers’s picture

Status: Active » Needs review
StatusFileSize
new24.26 KB
new12.31 KB

Pair programming with effulgentsia!

Initial patch, still rough, let's see what testbot says.

Status: Needs review » Needs work

The last submitted patch, 2: 2417647-2.patch, failed testing.

webchick’s picture

#2417333: Add support for user-path: scheme to Url class just got committed so this probably needs some adjustment.

Would also love even a single detail in the issue summary explaining why we are doing this. :)

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new14.44 KB
new3 KB

Status: Needs review » Needs work

The last submitted patch, 5: 2417647-5.patch, failed testing.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new20.38 KB
new7.73 KB

Status: Needs review » Needs work

The last submitted patch, 7: 2417647-7.patch, failed testing.

pwolanin’s picture

Issue summary: View changes
webchick’s picture

Thanks for the issue summary update. I still worry this creates confusion though, because it's non-standard from the other schemes.

For argument's sake, can't we just prefix the "/" in the link text field on display for "normal" paths? We will already be manipulating the value anyway to strip out the user-path: part prior to showing it.

For the front page, I would think that would be user-path:<front>
For the jump link, I would think that would be user-path:#jump

Don't want to block this necessarily, but I am concerned that this is going to result in DX headaches. :\ What am I missing?

pwolanin’s picture

Issue summary: View changes
Status: Needs work » Postponed

Yeah, we could potentially just do it for the widget UI for now, though It would be better to literally store the input the user types into the widget.

Discussing with effulgentsia in person, I think we should get the widget working even if it's not storing literal user input and then re-visiting this.

So, let's postpone this until the widget it working as we want. That means we will need to e.g. store user-path:<front> as the replacement for the typed / for now.

wim leers’s picture

Issue summary: View changes
Status: Postponed » Needs review
StatusFileSize
new20.33 KB
new2.25 KB

This fixes all remaining failures (barring new ones I've introduced), except for one, which will disappear once #2417793: Allow entity: URIs to be entered in link fields lands.

Marking to NR to send to testbot only, then moving back to postponed.

(Also posting an updated issue summary that I had finished 10 hours ago but couldn't post due to Newark International Airport having precisely zero functioning internets. It contains a more complete explanation, hoping that helps clarify the value of doing this. Posting this immediately after landing in Brussels, where 100% more internets are available to me.)

wim leers’s picture

Status: Needs review » Postponed

Status: Postponed » Needs work

The last submitted patch, 12: 2417647-12.patch, failed testing.

pwolanin’s picture

Status: Needs work » Postponed
wim leers’s picture

See #2418017-59: Implement autocomplete UI for the link widget, point 3. That issue tried to do something similar to add jumplink support, but in a backward way.

wim leers’s picture

wim leers’s picture

Assigned: Unassigned » wim leers
Priority: Major » Critical
Status: Postponed » Needs work
Issue tags: +blocker

This blocks #2418017, see #2418017-65: Implement autocomplete UI for the link widget, hence moving to critical, since that issue is critical.

wim leers’s picture

It's clear I wrote #12 while tired, because wow, that patch is *so* riddled with mistakes and inconsistencies it's not even funny.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new20.82 KB
new19.06 KB

And voila. This is much better.

There are two very large @todo blocks. They are a temporary translation layer, while the default link widget still shows NO leading slash in the UI. These @todo blocks can be removed in #2418017: Implement autocomplete UI for the link widget, for which this is a direct blocker.

wim leers’s picture

Title: Add leading slash to paths within 'user-path:' URIs » Add leading slash to paths within 'user-path:' URIs, to allow 'user-path:' URIs to point to the <none> route

Better title.

wim leers’s picture

Issue summary: View changes
wim leers’s picture

Assigned: wim leers » Unassigned
wim leers’s picture

These @todos can be deleted by #2418017: Implement autocomplete UI for the link widget. They make sure users can still enter <front> and <none> in the UI!

+++ b/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php
@@ -53,6 +53,20 @@ protected static function getUriAsDisplayableString($uri) {
+      // @todo Present the leading slash to the user and hence delete the next
+      //   block in https://www.drupal.org/node/2418017. There, we will also
+      //   remove the ability to enter '<front>' or '<none>', we'll expect '/'
+      //   and '' instead respectively.
+      $path = parse_url($uri, PHP_URL_PATH);
+      if ($path === '/') {
+        $uri_reference = '<front>' . substr($uri_reference, 1);
+      }
+      elseif (empty($path)) {
+        $uri_reference = '<none>' . $uri_reference;
+      }
+      else {
+        $uri_reference = ltrim($uri_reference, '/');
+      }

@@ -76,6 +90,31 @@ protected static function getUserEnteredStringAsUri($string) {
+        // @todo Present the leading slash to the user and hence delete the next
+        //   block in https://www.drupal.org/node/2418017. There, we will also
+        //   remove the ability to enter '<front>' or '<none>', we'll expect '/'
+        //   and '' instead respectively.
+        // Users can enter paths that don't start with a leading slash, we
+        // want to normalize them to have a leading slash. However, we don't
+        // want to add a leading slash if it already starts with one, or if it
+        // contains only a querystring or a fragment. Examples:
+        // - 'foo' -> '/foo'
+        // - '?foo=bar' -> '/?foo=bar'
+        // - '#foo' -> '/#foo'
+        // - '<front>' -> '/'
+        // - '<front>#foo' -> '/#foo'
+        // - '<none>' -> ''
+        // - '<none>#foo' -> '#foo'
+        if (strpos($string, '<front>') === 0) {
+          $string = '/' . substr($string, strlen('<front>'));
+        }
+        elseif (strpos($string, '<none>') === 0) {
+          $string = substr($string, strlen('<none>'));
+        }
+        elseif (!in_array($string[0], ['/', '?', '#'])) {
+          $string = '/' . $string;
+        }

Status: Needs review » Needs work

The last submitted patch, 20: 2417647-20.patch, failed testing.

wim leers’s picture

Status: Needs work » Needs review
StatusFileSize
new34.9 KB
new16.24 KB

Fixing all the test failures.

And grepped the code base for user-path:, made sure every single one of them was updated.
(Those occurrences that are like $entity = <EntityType>::create(['some_uri' => 'user-path:path'); $entity->save() — usually in tests — don't complain, because calling ->save() does NOT validate the values of the entity… it just saves them as-is.)

wim leers’s picture

To clarify: enabling the distinction below is what the issue is all about, it's what is required for jumplinks, which is what's missing from HEAD, and hence what is blocking #2418017: Implement autocomplete UI for the link widget.

+++ b/core/tests/Drupal/Tests/Core/UrlTest.php
@@ -613,10 +616,23 @@ public function testToUriStringForUserPath($uri, $options, $uri_string) {
+      // The four permutations of the special '<front>' path.
+      ['user-path:/', [], 'route:<front>'],
+      ['user-path:/', ['fragment' => 'top'], 'route:<front>#top'],
+      ['user-path:/', ['fragment' => 'top', 'query' => ['page' => '2']], 'route:<front>?page=2#top'],
+      ['user-path:/?page=2#top', [], 'route:<front>?page=2#top'],
+
+      // The four permutations of the special '<none>' path.
+      ['user-path:', [], 'route:<none>'],
+      ['user-path:', ['fragment' => 'top'], 'route:<none>#top'],
+      ['user-path:', ['fragment' => 'top', 'query' => ['page' => '2']], 'route:<none>?page=2#top'],
+      ['user-path:?page=2#top', [], 'route:<none>?page=2#top'],
effulgentsia’s picture

I discussed this a bunch with Wim and xjm, and here's what I believe to be the consensus at least between the 3 of us. Posting it here for feedback from others. TL;DR: +1 to this issue. Reasoning:

  1. Different schemes are different. Entities, non-entity Drupal pages, and non-Drupal pages are sufficiently different concepts that we should not be constrained to enforce syntax "consistency" in their identifiers. Just like tel:+1-201-555-0123 is not "consistent" with mailto:admin@example.com (leading + vs. not, - delimiter vs. @), each of our schemes can optimize its syntax within its own scope without concern for what the other schemes do.
  2. The semantics of user-path is to store a normalized representation of what the user intended to link to when typing a path. With that in mind, I think normalizing that intent into "absolute path" (with leading slash) notation is more logical than "relative path" (no leading slash) notation. When the user enters a path, I think it's clear that the most likely intent is a reference to a resource in the site's structure that is independent of the current Drupal resource that the visitor is viewing. I.e., if the link is to "about", that means "about" regardless of whether I'm currently on the front page, or the "user/login" page. It does not mean the "user/about" page when I'm on the "user/login" page. Such independence is common to represent with a leading slash.
  3. UIs may choose to expose the above or not. For example, the autocomplete link widget may have the user start paths with a leading slash in order to disambiguate from node titles which don't start with a leading slash. Another widget, however, might choose to not require users to start paths with a leading slash. This UI choice is independent of user-path syntax. So, whether user-path:/blog or user-path:blog is friendlier to UI users is irrelevant; it is only developers who are affected by this decision, and per above, I think with the leading slash is friendlier for developers due to its greater accuracy in modeling the underlying concept.
  4. We should not, however, add a leading slash to base. Or rather, we should follow the semantics of "base URI" defined by the RFC. Which basically translates into base:blog meaning relative to the subfolder that Drupal is installed in, and base:/blog meaning relative to the host (i.e., outside of Drupal's subdirectory).
  5. The above difference can be confusing if viewed side-by-side in code. For example, if 'blog' is implemented by Drupal and so you linked to it as user-path:/blog vs. if 'blog' is implemented by another script inside the same subdirectory as Drupal and so you linked to it as base:blog. However, once we add the methods suggested in #2417459: Provide internal API for special schemes and thin public wrappers for user input and non-routed URLs, you won't be viewing things like that side-by-side in code, so per point 1 above, this is almost a non-issue. I'll add a more detailed comment about that specifically in that issue.
dawehner’s picture

  1. +++ b/core/lib/Drupal/Core/Url.php
    @@ -319,6 +319,30 @@ protected static function fromEntityUri(array $uri_parts, array $options, $uri)
    +   * To clarify, a few examples of path plus corresponding 'user-path:' URI:
    +   * - 'node/add' -> 'user-path:/node/add'
    +   * - 'node/add?foo=bar' -> 'user-path:/node/add?foo=bar'
    +   * - 'node/add#kitten' -> 'user-path:/node/add#kitten'
    +   * - '<front>' -> 'user-path:/'
    +   * - '<front>foo=bar' -> 'user-path:/?foo=bar'
    +   * - '<front>#kitten' -> 'user-path:/#kitten'
    +   * - '<none>' -> 'user-path:'
    +   * - '<none>foo=bar' -> 'user-path:?foo=bar'
    +   * - '<none>#kitten' -> 'user-path:#kitten'
    +   *
    

    Great documentation.

  2. +++ b/core/lib/Drupal/Core/Url.php
    @@ -319,6 +319,30 @@ protected static function fromEntityUri(array $uri_parts, array $options, $uri)
    +   * URIs, we must ensure to not use it in case of the
    +   *
    

    ... this sentence stops in the middle of

  3. +++ b/core/modules/link/src/Plugin/Field/FieldWidget/LinkWidget.php
    @@ -53,6 +53,20 @@ protected static function getUriAsDisplayableString($uri) {
    +      // @todo Present the leading slash to the user and hence delete the next
    +      //   block in https://www.drupal.org/node/2418017. There, we will also
    +      //   remove the ability to enter '<front>' or '<none>', we'll expect '/'
    +      //   and '' instead respectively.
    +      $path = parse_url($uri, PHP_URL_PATH);
    +      if ($path === '/') {
    +        $uri_reference = '<front>' . substr($uri_reference, 1);
    +      }
    +      elseif (empty($path)) {
    +        $uri_reference = '<none>' . $uri_reference;
    +      }
    +      else {
    +        $uri_reference = ltrim($uri_reference, '/');
    +      }
    

    I'm not entirely sure whether its a good idea to remove the support for and in the link UI, given that other places like block visibility still supports it. Some kind of consistency is nice.

wim leers’s picture

StatusFileSize
new35.09 KB
new827 bytes
  1. :)
  2. Fixed.
  3. Well, it's a @todo that references another issue, so we can discuss it there :)
dawehner’s picture

Status: Needs review » Reviewed & tested by the community

I just assume it will be green as well

webchick’s picture

Status: Reviewed & tested by the community » Needs work

Shoot, I'm sorry, I saw #2216271: Regression: Shortcut links access is not checked first, so this needs a quick re-roll.

webchick’s picture

Issue tags: +Needs reroll
wim leers’s picture

Status: Needs work » Reviewed & tested by the community
Issue tags: -Needs reroll
StatusFileSize
new35.1 KB

Two merge conflicts, easy fixes.

webchick’s picture

While we're waiting for testbot (and these comments aren't going to block a commit):

Different schemes are different. Entities, non-entity Drupal pages, and non-Drupal pages are sufficiently different concepts that we should not be constrained to enforce syntax "consistency" in their identifiers. Just like tel:+1-201-555-0123 is not "consistent" with mailto:admin@example.com (leading + vs. not, - delimiter vs. @), each of our schemes can optimize its syntax within its own scope without concern for what the other schemes do.

Hm? These are both totally consistent. scheme: then paste in the thing you want to link to in full.

And in this respect I can actually understand not prefixing entity: with a slash, because what you want is entity:taxonomy_term/4 not entity:/taxonomy/term/4. Lack of leading slash makes it clear.

The semantics of user-path is to store a normalized representation of what the user intended to link to when typing a path. With that in mind, I think normalizing that intent into "absolute path" (with leading slash) notation is more logical than "relative path" (no leading slash) notation. When the user enters a path, I think it's clear that the most likely intent is a reference to a resource in the site's structure that is independent of the current Drupal resource that the visitor is viewing. I.e., if the link is to "about", that means "about" regardless of whether I'm currently on the front page, or the "user/login" page. It does not mean the "user/about" page when I'm on the "user/login" page. Such independence is common to represent with a leading slash.

Ok, so far so good...

We should not, however, add a leading slash to base. Or rather, we should follow the semantics of "base URI" defined by the RFC. Which basically translates into base:blog meaning relative to the subfolder that Drupal is installed in, and base:/blog meaning relative to the host (i.e., outside of Drupal's subdirectory).

Here's where you lose me. As a web developer for 20 years but one who doesn't have all of the various RFCs memorized (like at least 99.9% of web developers, I would reckon), it's frightfully confusing that when either way you're talking about putting in a link directly to a given URL, and when either way you want "a reference to a resource in the site's structure that is independent of the current Drupal resource that the visitor is viewing." So why would this not also be a leading slash, or else eliminate base: altogether in favour of just user-path: everywhere?

  • webchick committed d5304a4 on 8.0.x
    Issue #2417647 by Wim Leers, effulgentsia: Add leading slash to paths...
webchick’s picture

That said... the rationale for this patch makes a lot more sense now thanks to comment #34. And I guess the idea is that whether we bring consistency to base: or not doesn't really matter, because "in theory" if #2417459: Provide internal API for special schemes and thin public wrappers for user input and non-routed URLs makes it in, developers won't be dealing with these raw schemes directly anyway.

So let's get this in, and get to the end of the NJ sprint criticals ASAP so we can take a step back and look at the "big picture" once it's complete, and decide how we want to move from there.

Committed and pushed to 8.0.x. Thanks! See you in #2418017: Implement autocomplete UI for the link widget. :)

webchick’s picture

Status: Reviewed & tested by the community » Fixed

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.