Architecture
How the Drupal module, the CKEditor 5 plugin and the JavaScript library fit together.
The three pieces
drupal/anchor_link (this module)
|
|-- declares a CKEditor 5 plugin ...... anchor_link.ckeditor5.yml
|-- declares an asset library ......... anchor_link.libraries.yml
|-- alters core plugin definitions .... src/Hook/AnchorLinkHooks.php
|-- supplies a Linkit matcher ......... src/Plugin/Linkit/Matcher/
|-- widens allowed HTML on update ..... anchor_link.post_update.php
|
'-- loads -> vardot/ckeditor5-anchor-drupal (the editor behaviour)
libraries/ckeditor5-anchor-drupal/build/anchor-drupal.js
The division of labour matters when debugging: anything visible inside the editing surface is the library's job; anything to do with configuration, filtering, updates or Drupal integration is the module's.
Repository layout
| Path | Purpose |
|---|---|
anchor_link.info.yml |
Module definition. Depends on ckeditor5 and editor. |
anchor_link.ckeditor5.yml |
Declares the anchor_link_ckeditor5_anchor CKEditor 5 plugin, its toolbar item and the HTML elements it supports. |
anchor_link.libraries.yml |
cke5_anchor_link (the library JS) and admin.cke5_anchor_link (admin CSS). |
anchor_link.services.yml |
Registers the hook class with autowiring. |
anchor_link.post_update.php |
Post update that widens filter_html allowed HTML. |
anchor_link.module |
Thin legacy file; hooks live in src/Hook/. |
src/Hook/AnchorLinkHooks.php |
hook_help(). |
src/Plugin/CKEditor5PluginXToY/, src/Plugin/CKEditor4To5Upgrade/Anchor.php |
CKEditor 4 to 5 upgrade mapping. |
src/Plugin/CKEditorPlugin/ |
Legacy CKEditor 4 plugin classes (Anchor, Link, Unlink). |
src/Plugin/Linkit/Matcher/CKEditorAnchorLinkMatcher.php |
Linkit matcher for anchor suggestions. |
css/anchor_link.admin.css |
Admin-side styling, including the invisible anchor marker. |
js/ |
Legacy CKEditor 4 plugin assets and translations. |
tests/ |
varbase-e2e feature suite, step definitions, selectors, test recipe and PHPUnit tests. |
The CKEditor 5 plugin declaration
anchor_link.ckeditor5.yml is where Drupal learns the plugin exists:
anchor_link_ckeditor5_anchor:
ckeditor5:
plugins:
- anchorDrupal.Anchor
drupal:
label: Anchor link
library: anchor_link/cke5_anchor_link
admin_library: anchor_link/admin.cke5_anchor_link
toolbar_items:
anchor:
label: Anchor link
elements:
- <a>
- <a id="">
- <a name="">
- <a target="">
- <a rel="">
- <a class="ck-anchor">
Three things to note:
anchorDrupal.Anchoris the export from the external library. The module itself contains no CKEditor 5 source.elementsis what lets Drupal merge the needed attributes into a format's allowed HTML when the button is added.admin_libraryloads only while configuring a format, which is where the invisible anchor marker styling is needed.
Hook implementations
Hooks are OOP hooks (#[Hook] attributes) in src/Hook/AnchorLinkHooks.php,
registered as an autowired service.
hook_help()
Provides the module's help text at /admin/help/page/anchor_link.
Keeping General HTML Support away from the anchors
Core's General HTML Support would otherwise claim the <a> attributes the
anchor plugin owns. The plugin definition carries the guard itself, so it only
applies in editors where the Anchor button is enabled:
ckeditor5:
config:
htmlSupport:
disallow:
- name: a
attributes:
- id
- name
An editor without the Anchor button is left alone, so its anchors stay with
General HTML Support. The definition also loads link.LinkEditing and
link.LinkUI with the anchor plugin, because the library's AnchorUI requires
them - the editor attaches whatever the toolbar holds.
The ck-anchor class
ck-anchor is an editor-only marker. The library uses it to identify
anchors in the editing view so it can render the "INVISIBLE ANCHOR" flag and
make empty anchors selectable.
It is declared in the plugin's elements so the editor can use it, but it is
not written to the data view. Saved content contains the id (or name), not
the class. If it leaks into saved content, that is a library bug.
Anchors in saved markup
<!-- named span of text -->
<a id="pricing">Pricing</a>
<!-- empty anchor, renders nothing -->
<a id="pricing"></a>
<!-- a link that is also an anchor -->
<a id="see-also" href="/related">Related</a>
name is read as well as id, for content authored before id took over.