=== Link Picker for Etch ===
Contributors: tc
Tags: etch, internal links
Requires at least: 7.1.3
Tested up to: 7.1
Requires PHP: 8.3
Stable tag: 1.0.0-rc-1
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Link a native Etch Anchor to WordPress content, a term or an archive while retaining a normal href fallback.

== Description ==

Link Picker for Etch adds one control to the Etch Settings Bar, Link picker,
at the end of its top group of icons. It works only with normal
`etch/element` Anchors whose tag is `a`.

Author: [Tom Chmiel](https://etchdeveloper.com)
Plugin website and support: https://etchdeveloper.com/plugins/link-picker/
Version: 1.0.0-rc-1
Compatibility: WordPress 7.1.3 and Etch 1.7.0. Requires PHP 8.3 or later.
Etch must be installed and active to use the editor control.

The picker offers:

* published content of every publicly viewable post type: Pages, Posts and
  public custom post types,
* terms of every publicly viewable taxonomy, such as categories, tags and
  public custom taxonomies,
* the archive pages of post types that have one.

Private, draft, pending or scheduled content and files from the Media Library
are not offered.

Selecting Page 123 stores this deterministic contract on the Anchor:

`href="/current-permalink/"`
`data-tc-etch-link-post-id="123"`
`data-tc-etch-link-address="/current-permalink/"`

A term or an archive is stored in the same attribute with its kind:
`term:12` or `archive:book`. A plain number is always a post. The address is
the address of the target when you chose it, such as `/about/` or
`/?page_id=5`, without your query parameters and fragment.

While the plugin is active, the stored target decides where the link goes. A
link belongs to its target while its href is at the stored address: the same
path, ignoring case (also of letters outside the Latin alphabet, such as
Cyrillic), encoding and the final slash, with every query parameter of that
address. On frontend block rendering, the plugin then shows the current
address of the target, the permalink of the content or the link of the term
or archive: after a new slug, a new parent page or a new slug of a
parent, another permalink structure or a new slug of a term, also when other
content or a term took over the old address. A `#fragment` and your own query
parameters, such as `#contact` or `?utm_source=news`, are kept, also when you
add them in Etch. Content that exists but is not published is shown without
an href; a deleted target, or one that is no longer public, keeps the stored
href. Both attributes are removed from rendered HTML. Rendering never changes
the saved block content.

An href whose path you change in Etch no longer belongs to its target: the
Anchor is detached and the site shows your href exactly as typed. This applies
to another site path, a full URL (even one on this site), `#section`,
`mailto:`, `tel:`, an empty href and dynamic Etch values such as
`{props.url}` or `{item.url}` in a loop: Etch fills in such a value before
the plugin sees the link, so the plugin checks the href saved in the block.
The plugin does not check what the site shows at an address today. Site
Health lists detached Anchors.

No custom tables, post types, shortcodes, dynamic-data expressions, visitor
scripts, or persistent link registries are used. The updater stores
only a temporary, network-wide cache of public release information.

= In the picker =

* Search by title, slug, ID, path or a pasted link. Terms are found by name,
  slug, ID or the last segment of their path, and archives by name. The search
  runs on the server and returns 50 results at a time; Show more adds the
  next 50. A search can be up to 200 characters long, a pasted link or path up
  to 2,048; the #fragment of a pasted link is ignored. A pasted link finds
  what it leads to by simple rules, see the limitations in the FAQ: published content as
  WordPress finds it at that address, a term at its link and an archive at
  its address; a link to another site finds nothing. A link to the Etch
  builder or to an edit screen of WordPress, such as `/?etch=magic&post_id=5`,
  `/wp-admin/post.php?post=5&action=edit` or a page in the Site Editor, finds
  what it edits.
* Filter by kind: All, each post type, each public taxonomy and Archives. All
  lists content, terms and archives together, alphabetically, and counts the
  results of every filter. All and up to five post types share one row; when
  there are more than six filters, the other post types, the taxonomies and
  Archives are in the More menu. Filters without results are not shown. The
  chosen filter is kept while Etch is open, unless it would hide the current
  target of a link.
* Each result shows an icon of its kind, its title with the search term
  marked, its address, its kind and its ID, for example "News",
  "/category/news/", "Category" and "ID 12". Archives have no ID.
* With an empty search field, the current source and up to five recently used
  targets are listed first, under Recent.
* Click a result to select it. Nothing changes in Etch until you choose Insert
  link, or Update link when the Anchor already has a source link. A
  double-click on a result selects and inserts it at once. Links to shows the
  address the link will have, with the query parameters it keeps.
* Insert link, Update link and Remove source link change the Anchor in Etch
  like any other edit, in one step that Undo reverts. The picker does not
  save the page: save it in Etch to keep the change.
* When the Anchor has a source link, Current names the linked content and the
  stored href, and that content is selected when the picker opens. Remove
  source link removes `data-tc-etch-link-post-id` and
  `data-tc-etch-link-address` and keeps what the site shows, like Remove all
  source links in Tools: an outdated href becomes the address that the site
  shows, and a link to unpublished content, which the site shows without an
  address, loses its href too. Any other href stays. Before you click it,
  Current says what the link becomes, such as After removal: /new-address/#x.
* Add an optional fragment, such as `contact`; a typed `#` is removed. It is
  prefilled from the current href for the linked content while the link
  belongs to it. Otherwise, for an Anchor without a source link or with one
  that has no effect, such as a changed href, a source link that is no ID or
  one whose content was deleted, it is prefilled for the content that the
  href leads to by the simple rules of the search, also when the href is a
  full URL of this site; it never goes to content that the href does not
  lead to. Choosing other content clears it, also an ID chosen below, unless
  you typed it. For content, IDs on this page offers up to 20 ids of Etch
  elements on the selected page; elements inside loops, conditions and
  components are not offered. Choosing the same content again also keeps your
  query parameters, and so does choosing the content that the href leads to,
  such as About for `/about/2/`, `/?page_id=5` or
  `https://example.com/about/`. Insert link and Update link wait for the
  site's answer about the selected content, also after a quick double-click
  or Enter, Enter.
* Turn on Open in a new tab to set `target="_blank"` and `rel="noopener"`.
* When the stored address differs from the address the site shows, the picker
  tells you which one visitors get. It also tells you when the href was
  changed in Etch, so the site shows it as typed, or its stored address is
  missing, empty or no site path, and when the linked content
  was deleted, unpublished or can no longer be linked. It also explains a source
  link that is no ID, such as `abc`, or a dynamic value, such as `{item.id}`.
* When the href is a dynamic Etch value, the picker warns you; with a source
  link, it also says that the site shows the value. For a property of the
  component, such as `{props.url}`, it says that a fixed address would
  replace it in every instance. Replacing it requires ticking Replace the
  dynamic href.

= Site Health =

Tools > Site Health includes Link Picker for Etch tests. They scan
published content, including Etch components and templates, and list up to 20
Anchors each, with an edit link where WordPress provides one:

* links whose target was deleted, unpublished or is no longer public or
  linkable, for example a deleted term or the archive of a post type that no
  longer has one,
* links whose saved address is outdated, so they would point to an old address
  if the plugin were deactivated; another encoding of the address, such as
  `%D0%BB` for `л`, or Latin letters in another case are not outdated,
  because WordPress finds the content there,
* Anchors whose source link has no effect because their href is not a site
  path or was changed in Etch, their stored address is missing or empty, or
  the element is no longer an Anchor. Each one says why.

= Tools > Link Picker for Etch =

This page lists every source-linked Anchor, including drafts, components and
templates, with its target and status, and why an inactive source link has no
effect; equal Anchors of one page are listed each.

Before you deactivate or delete the plugin, you can:

* Update saved addresses: store the address the site shows in every outdated
  Anchor, with the current address of its target, keeping the fragment, your
  query parameters and the source link. An href changed in Etch is left as it
  is. Spaces around a saved address, which browsers ignore, do not make it
  outdated.
* Remove all source links: update the addresses and remove every
  `data-tc-etch-link-post-id` and `data-tc-etch-link-address`, leaving
  ordinary links. A link to unpublished content, which the site shows without
  an address, loses its href too.

Each action is previewed first and changes nothing until you confirm it. It
runs in batches and needs the right to manage options and to edit each item.
When WordPress keeps revisions of an item, the previous content is saved as a
revision; the page warns when revisions are turned off. Only the changed
attribute values are rewritten. After saving, the tool compares the stored
content with the expected content byte for byte and reports every item that
differs, for example because another plugin changed it on save. Nothing runs
automatically when the plugin is deactivated or deleted.

= Warnings before deleting linked content =

On the Pages, Posts and custom post type lists, moving content to the Trash
or deleting it permanently asks for confirmation when published Etch content
links to it, with the number of linking Anchors and what happens to them: in
the Trash they are shown without an address; after a permanent deletion they
keep their last saved address, which no longer works. The same applies to
Delete on the term lists of public taxonomies. This covers single items and
bulk actions; a selection of more than 50 items is counted in parts. Without
links, WordPress continues as usual.

When linked content leaves publication, also when published content is
deleted without the Trash, such as through the REST API with `force=true` or
with `wp post delete --force`, or when a linked term is deleted, from any
screen, the next admin page shows a warning with a link to Site Health. In
WP-CLI, the command prints the warning. The plugin counts links on demand and
keeps the warning only briefly, for the user who made the change.

There is no settings page.

== Installation ==

1. Install and activate Etch.
2. In WordPress, open Plugins > Add New Plugin > Upload Plugin, choose
   link-picker-for-etch-1.0.0-rc-1.zip, install it, and activate Link Picker for Etch.
3. In Etch, select a normal Anchor.
4. Click Link picker, the chain with a magnifying glass at the end of the top
   group of the Settings Bar.
5. Search for and select published content, a term or an archive, then
   choose Insert link.
6. Save the page in Etch.

== Frequently Asked Questions ==

= What happens when I deactivate the plugin? =

Your links keep working with the address saved in each Anchor. If content was
renamed or moved while the plugin was active, that saved address can be
outdated: WordPress redirects the old address of a renamed Post, but not of
a Page or other hierarchical content. Open
Tools > Link Picker for Etch and choose Update saved addresses first. To leave
no plugin data in your content, choose Remove all source links.

= Can I link to categories, tags and archives? =

Yes. Terms of public taxonomies and the archive pages of post types that have
one are offered next to Pages and Posts. Links to a term follow it when its
slug changes, like links to content.

= Can I link to a PDF or another file? =

Not with the picker. WordPress keeps a file's URL until the file is deleted,
so paste the file URL from the Media Library as an ordinary href.

= Can I link to a section of a page? =

Yes. Select the content, then type the fragment, for example `contact`, in
the Fragment field, or choose one of the IDs on this page. It stays when the
content's address changes.

= What happens when I rename a page? =

Links follow it. Each link stores the address of its content when you choose
it, and the site shows the current address of that content while the href is
at the stored address: after a new slug, a new parent page or a new slug of a
parent, another permalink structure, and also when other content or a term
takes over the old address.

= Will the plugin overwrite an address I typed by hand? =

No. If you change the path of the href in Etch, for example `/about/` typed
into a button that was linked to Contact, the link no longer belongs to its
content and the site shows your href as typed. That includes a full URL,
`#section`, `mailto:`, `tel:` or a dynamic Etch value. A fragment or query
parameters that you add, such as `#team` or `?utm_source=news`, keep the link
to its content. Site Health lists the Anchors you changed, so you can remove
their source link or choose content again.

= Can I use it with dynamic data or component properties? =

The picker writes a fixed address. If an Anchor's href is a dynamic Etch value,
such as `{props.url}` or `{item.url}` in a loop, the site shows the value that
Etch fills in, also when the Anchor has a source link. The picker warns you
and replaces it only after you confirm. A fixed address in place of a
property of a component is the same in every instance of the component; to
link one instance, set the property on that instance. Linking component
properties is not supported.

= Does the picker save the page? =

No. Insert link, Update link and Remove source link change the Anchor in Etch
like any other edit, and Undo reverts them. Save the page in Etch to keep the
change; until then, the site shows the previous link. Other unsaved changes
and component definitions are saved only when you save them. Etch asks before
you leave the builder with unsaved changes, and a changed link is one.

= What happens when the selection is not an Anchor? =

When Link picker is clicked and the selection is not an Anchor, or nothing is
selected, a notice beside the control explains this and names the selected
element. For a component, it explains that links inside it are set with Edit
Component, and a link whose address is a property of the component in that
property of the instance. When the selected element is inside an Anchor, such
as the heading or the text of a card link, the notice offers Open Link picker
for that Anchor, which selects it in Etch and opens the picker. The notice
does not block the editor: only its buttons take clicks, and a click anywhere
else on it reaches the canvas or panel under it. It moves with the control
when the window is resized. While it is open, it follows the selection: it
names another selected element, and selecting an Anchor opens the picker for
it. After a key press in the builder, the picker opens once the typing stops.
Etch changes the tag with every key typed in its Tag field, so after a key
typed in a text field the picker waits for Enter or Tab there, a click
elsewhere or the focus leaving the field; meanwhile the notice names the
Anchor. Close the notice with its X button, with Escape or by clicking Link
picker again. Escape closes it from the notice, from Link picker or when no
field has focus; in a field or list of Etch, such as the Tag list, Escape
belongs to Etch. The notice closes when Etch shows another place, such as the
Style Manager. With the keyboard, Tab on Link picker moves into the notice, as
if it came right after the control.

Etch shows Link picker in each of its places, such as Templates or the Style
Manager, beside the buttons that switch between them. Outside the builder,
Link picker first shows the builder, then opens the picker for its selected
Anchor.

= Which keys does the picker use? =

Arrow Down moves from the search field to the results, Arrow Up and
Arrow Down move between results, and Home and End jump to the first and last
result. The results are one stop for Tab, held by the selected result or the
one that had focus last. Enter in the search field selects the first result,
and Enter on a result selects it; Enter on the selected result or in the
Fragment field inserts it, and so does Ctrl+Enter (Cmd+Enter on macOS)
anywhere in the picker. Escape closes the More menu, then clears the search,
then closes the picker, without changing the selection in Etch. While the
picker is open, no key reaches Etch's shortcuts, so Delete does not delete the
Anchor under it. Etch's shortcuts with Ctrl or Cmd, such as Ctrl+S, do not
reach the browser either, so it does not open Save page as. In a window less
than 768 pixels high, the picker is more compact and each result takes one
line, so the list shows at least four results. In a window lower than about
650 pixels, Insert link stays in view, and what does not fit scrolls under it,
with a shadow while there is more below. In a window about 300 pixels high,
moving into the results scrolls the picker so that it shows four of them.

When Insert link, Update link or Remove source link cannot change the
Anchor, for example because it was deleted while the picker was open, the
picker says why, and the focus returns to the control you used.

= Does it work on large sites? =

It was measured in WordPress Playground (PHP as WebAssembly, with SQLite)
with 10,000 published Pages and Posts, 2,000 terms, 2,000 Media Library files,
three archives and 2,000 linked items with six or seven Anchors each. A page
of picker results took at most 0.1 seconds, the Site Health tests 2.5 seconds
together, counting links for a list of 20 items before a Trash action 1.2
seconds and each batch of Update saved addresses at most 2.7 seconds. No step
used more than 69 MB of memory.

= Does it support multilingual sites? =

There is no built-in integration with translation plugins. Developers can map
targets with the hooks below.

= Developer hooks =

`link_picker_for_etch_target_id`, `link_picker_for_etch_target` and
`link_picker_for_etch_href` change only the rendered HTML; Site Health and the
exit tool use the stored target and address. `link_picker_for_etch_post_types`
and `link_picker_for_etch_taxonomies` also apply to the picker, Site Health
and the exit tool.

* `link_picker_for_etch_post_types` (filter, array of post type names): limits
  the post types the picker offers and the frontend resolves, including their
  archives. Types that are not publicly viewable, and attachments, are ignored.
* `link_picker_for_etch_taxonomies` (filter, array of taxonomy names): limits
  the taxonomies whose terms the picker offers and the frontend resolves.
  Taxonomies that are not publicly viewable are ignored.
* `link_picker_for_etch_target_id` (filter, int `$post_id`, string
  `$stored_href`): the content an Anchor links to, for example a translation.
  Return a positive post ID; anything else keeps the stored target. Posts only.
* `link_picker_for_etch_target` (filter, array `$target`, string
  `$stored_href`): the term or archive an Anchor links to, for example a
  translated term. `$target` is `array( 'type' => 'term', 'id' => 12 )` or
  `array( 'type' => 'archive', 'post_type' => 'book' )`. Return another array
  of this form; anything else, including a post, keeps the stored target.
* `link_picker_for_etch_href` (filter, string `$href`, int `$post_id`, string
  `$stored_href`): the final frontend href of a link to content. Return a
  non-empty string. Posts only.
* `link_picker_for_etch_load_editor` (filter, bool `$load`): whether the
  editor assets load on the current request, by default only in the Etch
  builder (`?etch=magic`).

Example for a multilingual site, here with Polylang's `pll_get_post()`:

    add_filter( 'link_picker_for_etch_target_id', function ( $post_id ) {
        if ( ! function_exists( 'pll_get_post' ) ) {
            return $post_id;
        }
        $translation = pll_get_post( $post_id );
        return $translation ? $translation : $post_id;
    } );

= How are updates delivered? =

While Link Picker for Etch is active, WordPress checks the official release
feed during its normal plugin update checks. New releases appear under Plugins
and Dashboard > Updates, with an Update now link and a version details dialog.
Use Check for updates next to Link Picker for Etch, or Dashboard > Updates >
Check again, to request fresh release information. On multisite,
network-activate Link Picker for Etch for network-wide checks and manage updates
in Network Admin.

Automatic updates are optional: use Enable auto-updates in the Plugins list
when this control is available under your site's WordPress policy. Link Picker
for Etch does not enable automatic installation on your behalf. WordPress manages ZIP
downloads, filesystem permissions, replacement and its usual update checks.

The official website is https://etchdeveloper.com/plugins/link-picker/.
The release feed is https://etchdeveloper.com/plugins/link-picker/update.json.
Updates start being available after the publisher uploads that JSON file and
the release ZIP. An unavailable feed does not interrupt the picker or frontend.

For a manual update, upload the new ZIP through Plugins > Add New Plugin >
Upload Plugin and choose Replace current with uploaded. This also works when
the plugin is inactive.

Keep the technical folder link-picker-for-etch and main PHP file
link-picker-for-etch.php unchanged in future releases. They identify the
installation. Do not uninstall the plugin as part of a normal update.

Your server must reach etchdeveloper.com over HTTPS. WordPress checks for
updates on its own schedule, normally twice a day while its scheduled tasks
run.

= What does the plugin send or store? =

The updater requests public release metadata from etchdeveloper.com over HTTPS
only during WordPress update checks or when opening release details. It sends
a Link-Picker-for-Etch/version User-Agent and no site URL, user data, page content,
credentials or analytics payload. The publisher's server can see the requesting
server's IP address and ordinary HTTP access logs. WordPress downloads the ZIP
from the same official domain when an update is installed.

Successful metadata is cached for six hours; failures for ten minutes. Check
again clears this local cache. The updater adds no tracking scripts or cookies.

The picker remembers up to five recently selected targets (post IDs and
values such as term:12) in the editor's browser, under the localStorage key
tceil-recent-targets. They are sent only to your own site's REST API, to list
those targets first, and are removed by clearing the site's browser data. A
warning about linked content that was unpublished or deleted, or a deleted
linked term, is kept for up to ten minutes for the user who made the change, as a WordPress
transient: in the options table, or in the persistent object cache when the
site uses one.
Uninstalling clears its release cache and leaves saved links and content
intact.

= How do links survive deactivation or a migration? =

Disabling the plugin stops live permalink resolution, but it does not damage
links: each Anchor still has the last persisted ordinary href. No proprietary
expression is stored in href.

The persisted href changes only when content is chosen again in the picker and
the page is saved in Etch, or when Tools > Link Picker for Etch updates saved
addresses. Use that tool before deactivating, because WordPress does not
redirect old addresses of Pages and other hierarchical content.

The migration fields are `data-tc-etch-link-post-id` and
`data-tc-etch-link-address`. A future native Etch migration can read the
target directly, write Etch's future documented source format, and then
remove this plugin and both custom attributes.

= What are the limitations? =

* Lists only post types and taxonomies that WordPress reports as publicly
  viewable.
* Live resolution applies to markup that passes through WordPress's
  `render_block` filter. A dynamic href is recognized in the block that the
  filter receives; an Anchor in other markup is resolved as rendered.
* The editor assets are delivered only on the request that opens the Etch
  builder (`?etch=magic`), to logged-in users who can edit Pages. Lightweight
  readiness checks back off to every two seconds and stop after about one
  minute when no usable Etch runtime appears. They access only the current
  window.
* A deleted, invalid, or unresolvable post ID keeps the existing href, as does
  content whose post type is no longer registered or public. The same applies
  to a deleted term or archive, and a taxonomy or post type that is no longer
  public. Existing Pages, Posts and other linkable content
  that are no longer published (including trash) have href removed from
  rendered HTML only. Stored content is not rewritten.
* The confirmations before a deletion cover the rows and bulk actions of
  the content and term lists. Empty Trash asks nothing: content in the
  Trash was reported when it left publication. The warning after a change
  appears wherever the change is made.
* A link follows its target only while its href is at the stored address. A
  path that you change in Etch, also one that leads to the same content, such
  as `/about/2/`, detaches the link on purpose; choose the content again to
  link it. So does a missing `data-tc-etch-link-address`, for example next to
  a source link typed by hand.
* Without the PHP mbstring extension, case is ignored only for Latin letters,
  as WordPress itself then does.
* Content copied from another site keeps its stored IDs and addresses: a link
  follows the content that has that ID on this site, also when it is other
  content than on the site it came from.
* The search, and the fragment and query that the picker keeps for an href
  whose source link has no effect, read a link by simple rules: published
  content as WordPress's `url_to_postid()` finds it, a term whose slug is the
  last segment of the path at its link, and an archive at its address. Other
  pages of content, such as its feed or page 2, count as that content, and a
  link to the home page with a query, such as `/?pagename=about`, as the
  front page; former addresses, plain links of terms, such as `/?cat=5`, and
  paths with encoded letters, such as `/about/%32/`, find nothing. These
  rules do not decide what the site, Tools or Site Health show.
* Site Health reads Etch block attributes from published content; the exit
  tool also reads drafts. Neither checks links in other plugins' data.
* Files from the Media Library and component properties cannot be linked.
* There is intentionally no translation plugin behavior; see Developer hooks.
* There is intentionally no speculative future Etch migration.

= What is the public Etch contract? =

The editor integration uses only these current public APIs:

* `window.etch.blocks.getSelectedId()`
* `window.etch.blocks.getJson()`, including its `parentId`
* `window.etch.blocks.select()` (when available)
* `window.etch.blocks.getTree()` (when available)
* `window.etch.blocks.getAttribute()`
* `window.etch.blocks.setAttribute()`
* `window.etch.blocks.removeAttribute()`
* `window.etch.navigation.getActivePostId()` (when available)
* `window.etch.navigation.getCurrentPlace()` and `.goTo('builder')` (when
  available)
* `window.etchControls.builder.settingsBar.top.addAfter()`, or
  `.center.addAfter()` where Etch has no top section
* `window.etchControls.builder.settingsBar.top.before` and `.after` (or those
  of the center section), read only (when available)
* WordPress actions `wp_enqueue_scripts` and `admin_enqueue_scripts`, used only
  on the request that opens the Etch builder: an address of the site with
  `?etch=magic`

The plugin's own REST routes, available to users who can edit Pages:

* `GET /wp-json/tc-etch-internal-links/v1/targets?search=&filter=&page=&pinned=`
  lists published content, terms and archives, 50 per page. `search` takes up
  to 200 characters, or 2,048 for a link or path. `filter` is empty (All),
  `type:{post_type}`, `taxonomy:{taxonomy}` or `archives`; `pinned` takes up to
  six target values.
* `GET /wp-json/tc-etch-internal-links/v1/target?value=&href=&address=&linked=`
  resolves one target, such as `123` or `term:12`, before it is saved. For
  content the user must be able to edit the target. Its `href` is the address
  that the picker stores in `data-tc-etch-link-address`. Its `anchors` lists
  up to 20 id attributes of Etch elements in the saved content of that
  target, outside loops, conditions and components; it is empty for terms and
  archives. With the optional stored `href` of an Anchor, `href_leads` tells
  whether that href leads to the target by the simple rules of the search.
  With `linked=1`, the Anchor is linked to the target, and `href_status`
  (`ok`, `stale` or `detached`), `href_reason` (for a detached link:
  `not_anchor`, `dynamic`, `not_path`, `invalid_address` or `changed`) and
  `live` tell what the site shows for it, as in Site Health, by its stored
  `address`; without `address`, the link is detached (`invalid_address`). A
  target that cannot be linked, including a deleted
  post, is not found (404); the error data names the `reason`, the `title`
  while the target exists and, with `linked`, its `href_status` and
  `href_reason`.
* `GET /wp-json/tc-etch-internal-links/v1/inbound?ids=1,2,3` counts links in
  published content to each ID the user may delete, each Anchor once;
  `targets=term:12,term:15` does the same for terms. It takes up to 50 values
  and requires the right to edit posts; the list screens count a larger
  selection in parts.

The implementation feature-detects every Etch method it calls. The current
published Etch capability contract has no native internal-link capability, so
native-link handoff is intentionally not guessed or implemented.

Etch 1.7.0 forwards only the icon, tooltip and callback of a Settings Bar
control, so its accessible name is not rendered, and it offers no separator or
pressed state. A DOM adapter names the plugin's own button, found by its
position among the section's after controls, counted from the end (or,
without that registry, as the button added after registration that shows the
plugin's icon). The notice uses the same button to appear beside it, or the
bottom left corner when the button is not found. Tab on that button moves
into the notice, and Tab on the last button of the notice moves on to what
follows the button; Shift+Tab from there returns to the notice. The adapter
observes only
that section and the direct children of its ancestors. Recheck this adapter
when upgrading Etch's controls implementation.

Etch documents Iconify names for a control's icon. The plugin passes its own
SVG as Iconify icon data (`body`, `width` and `height`), which Etch 1.7.0
renders like a named icon; if Etch refuses icon data, the control uses the
Iconify name `iconoven:link-search`. Recheck the icon when upgrading Etch.

The notice names the selected block by the `tag`, `type` and `context.name`
that `getJson()` returns. Etch has no public selection event, so while the
notice about the selection is open, the plugin reads `getSelectedId()` every
250 ms, and reads the block again only when the selected id changes or after
a click or key press in the builder. The editor stylesheet uses Etch's CSS custom
properties `--e-font-interface`, `--e-foreground-color`, `--e-border-color`,
`--e-primary` and `--e-text-color-dark` when they exist, with its own
fallbacks, and styles only its own `tceil-` classes.

The picker never saves: it changes the Anchor's attributes, also inside a
component being edited, and Etch's Save keeps them. It changes them all at
once, which Etch 1.7.0 records as one step of its history. Before a change, it checks
that the Anchor is still in the page with `getTree()`, because `getJson()`
still returns a deleted block. While the picker is open, it keeps keyboard
events in capture phase on the window from Etch's shortcuts; the keyup of a key
pressed before it opened still reaches Etch. For Ctrl or Cmd with S, D, K, M
or I, which Etch keeps from the browser, the picker prevents the browser's
action too. The notice reads the parents of the selected block with
`getJson()` to find an Anchor around it, and selects that Anchor with
`select()`. With only the notice open, the plugin takes an Escape only from
the notice, from its own button or when no element has focus.

A source link stored as a JSON number, such as `1670` set with
`setAttribute()`, counts as that ID in the picker, as on the site.

== License and support ==

Copyright (C) 2026 Tom Chmiel.
Link Picker for Etch is free software licensed under the GNU General Public
License, version 2 or (at your option) any later version (GPL-2.0-or-later). It
is distributed without warranty. The complete license is included in
LICENSE.txt. Etch and WordPress are separate products with their own licenses.

For documentation and support, visit the official plugin website. When reporting
a problem, include the Link Picker for Etch, WordPress, Etch, PHP and browser
versions,
steps to reproduce, and the relevant error message. Do not send credentials.

== Changelog ==

= 1.0.0-rc-1 =

* First public release, published as a release candidate for 1.0.0. Please
  report problems through the plugin website.
