The Linkit Matcher
Drupal\anchor_link\Plugin\Linkit\Matcher\CKEditorAnchorLinkMatcher supplies
anchor suggestions to the Linkit
autocomplete.
Plugin definition
/**
* @Matcher(
* id = "ckeditor_anchor_link",
* label = @Translation("CKEditor Anchor link"),
* )
*/
class CKEditorAnchorLinkMatcher extends MatcherBase
It extends MatcherBase, so it has no target entity type and issues no
queries. Linkit remains an optional dependency: the class is only instantiated
when Linkit is installed and the matcher is added to a profile.
execute($string)
Takes the current search string and returns a SuggestionCollection.
public function execute($string) {
$suggestions = new SuggestionCollection();
$string = ltrim((string) $string, '#');
// A search naming a scheme or a path is a link to somewhere else, and an
// id carries no whitespace, so none of those describe a fragment on this
// page and there is nothing to suggest.
if ($string === ''
|| preg_match('#^[a-z][a-z0-9+.\-]*:#i', $string)
|| str_contains($string, '/')
|| preg_match('/\s/', $string)) {
return $suggestions;
}
$suggestion = new DescriptionSuggestion();
$suggestion->setLabel($this->t('#@anchor_link', ['@anchor_link' => $string]))
->setPath('#' . $string)
->setGroup($this->t('Anchor links (within the same page)'));
$suggestions->addSuggestion($suggestion);
return $suggestions;
}
The cast
(string) $string before ltrim() is deliberate. Linkit can call the matcher
with NULL when the field is empty, and passing NULL to ltrim() is
deprecated as of PHP 8.1. Without the cast the site logs a deprecation on every
empty autocomplete request.
The guard clauses
Each clause rules out a search that cannot describe a fragment on the current page:
| Clause | Rejects | Example |
|---|---|---|
$string === '' |
Nothing typed, or only a # |
# |
^[a-z][a-z0-9+.\-]*: |
Anything with a URI scheme | https://, mailto: |
str_contains($string, '/') |
Anything with a path separator | /about/team |
preg_match('/\s/', ...) |
Whitespace, which an id cannot contain | two words |
Returning an empty collection means Linkit simply shows no anchor suggestion,
which is the intended behaviour. Before this guard existed the matcher offered a
#https://example.com suggestion for every pasted URL.
The scheme pattern follows RFC 3986: a letter followed by letters, digits, +,
. or -, then a colon.
The suggestion
A DescriptionSuggestion with:
- label -
#plus the cleaned string, so the editor sees what they will get - path -
#plus the cleaned string, which is what gets inserted - group - "Anchor links (within the same page)", which groups anchor suggestions separately from entity results in the autocomplete
Current limitations
The matcher is a transformer, not a finder. It turns what the editor typed into a fragment; it never looks at content to see whether that anchor exists.
Consequences:
- No discovery. The editor must already know the anchor name.
- No validation. A typo produces a confident suggestion for a target that does not exist.
- Same-page scope only, which is why the group is labelled that way.
Proposed rework
#3460457 Rework the Suggestion for CKEditor Anchor Link Matcher class for
Linkit proposes
replacing this with a matcher that extends Linkit's EntityMatcher and queries
real content for anchors.
Sketch of the proposal:
- extend
EntityMatcherwithtarget_entity = "node"; - query
text_longandtext_with_summaryfields for the anchor marker, one query per bundle to keep the join count down; - optionally recurse into
entity_reference_revisions(paragraph) fields, with a configurable depth, because each discovered field adds a join; - configuration for bundle filtering, bundle grouping, result limit, metadata and whether unpublished nodes are included;
- access checks on both the query and each loaded entity.
Open questions on the issue: the cost and complexity of the generated SQL on large sites, whether anchors on the current unsaved node should be found client-side instead, and whether same-page and site-wide suggestions should be two separate features rather than one matcher.
The maintainer's position on the issue is that it is a good direction and a big change (an entity query, the field manager and a settings form), so it should land with test coverage rather than as a quick merge.
If you depend on the current same-page behaviour, note that the rework would change it. Follow the issue.
Writing your own matcher
If you need different behaviour now, add a matcher rather than patching this one:
namespace Drupal\my_module\Plugin\Linkit\Matcher;
use Drupal\linkit\MatcherBase;
use Drupal\linkit\Suggestion\DescriptionSuggestion;
use Drupal\linkit\Suggestion\SuggestionCollection;
/**
* @Matcher(
* id = "my_anchor_matcher",
* label = @Translation("My anchor matcher"),
* )
*/
class MyAnchorMatcher extends MatcherBase {
public function execute($string) {
$suggestions = new SuggestionCollection();
// Build and add your own suggestions.
return $suggestions;
}
}
Add it to the Linkit profile alongside, or instead of, the one shipped here.