WP Manifestindependent plugin directory
manifest / content / acronym-ui

Acronym UI

WordPress-плагин: оборачивает акронимы и сокращения в <abbr> с настраиваемой подсказкой.

by llm-agent.ru · github.com/geliorn/acronym-ui

★ 0stars
0forks

Install

No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:

wp plugin install https://github.com/geliorn/acronym-ui/archive/refs/heads/main.zip

Оборачивает акронимы и сокращения в постах и комментариях в <abbr title="…"> с настраиваемой подсказкой: пунктирное подчёркивание, значок [?], тултип по hover/focus на десктопе и по клику/тапу на мобильных. Переписан с нуля на основе Acronyms 2 (Chris Cox, GPLv2+) под современный PHP 8 / WordPress — с сохранением исходной идеи (глоссарий терминов + замена в контенте/комментариях + управление списком в админке), но без её проблем.

Содержание

Что изменилось относительно оригинала

Оригинал (acronyms.php, 2.0.10) заменял акронимы одним preg_replace на каждый акроним из словаря прямо по сырому HTML, с лукэраундами вида (?!<[^<>]*?)(?<![?.&])\bACRONYM\b(?!:)(?![^<>]*?>). Эта конструкция защищает только от попадания внутрь тега (в атрибуты) — но не от попадания в текст заголовка или ссылки, поэтому акронимы в <h2>API</h2> или <a>API</a> подсвечивались точно так же, как в обычном абзаце. Плюс: тег <acronym> для не-HTML5-тем (сам <acronym> убран из спецификации HTML5, так что ветка была мёртвым кодом уже много лет), действия админки через $_GET, несогласованные capability-проверки (edit_others_posts для страницы, manage_categories для удаления, ничего — для добавления/правки кроме nonce), attribute_escape() вместо esc_attr(), вывод в таблицу без экранирования, register_uninstall_hook на класс в главном файле, самодельная HTML-таблица с пагинацией.

Изменения в этой версии:

Область Было (Acronyms 2) Стало (Acronym UI)
Разбор HTML один preg_replace с лукэраундами на весь HTML, в цикле по каждому акронизму HTML разбирается на теги/текст-токены; замена только в тексте вне «запрещённых» тегов (class-aui-replacer.php)
Заголовки и ссылки подсвечивались наравне с обычным текстом исключены явно (h1–h6, a), см. Алгоритм замены
Производительность N вызовов preg_replace за рендер (по числу акронимов в словаре) 1 комбинированный regex-паттерн (alternation) на все акронимы, 1 проход по каждому текстовому токену
Повтор одного акронима подсвечивалось каждое вхождение настраиваемый лимит max_highlights (по умолчанию 1 — только первое вхождение на странице)
Тег разметки <acronym> или <abbr> в зависимости от current_theme_supports('html5') всегда <abbr>
Подсказка на мобильных нет (title не работает без hover) кастомный JS-тултип, открывается по тапу
Стилизация нет вообще пунктирное подчёркивание + значок [?], оба вкл/выкл и настраиваемы (цвет, глиф)
Изменение данных в админке GET-запросы (add-acronym, edit-acronym, …) POST через admin-post.php, везде nonce + current_user_can('manage_options')
Список акронимов в админке самодельная HTML-таблица + ручная пагинация WP_List_Table (поиск, сортировка, пагинация, bulk-delete)
Удаление плагина register_uninstall_hook на класс в главном файле отдельный uninstall.php
Данные старого плагина — переносятся один раз при активации, если найдены

Архитектура и карта файлов

plugins/acronym-ui/
├── acronym-ui.php                   # заголовок плагина, константы, require, boot-функция, activation hook
├── uninstall.php                    # чистка опций при удалении плагина
├── README.md
├── includes/
│   ├── class-aui-repository.php     # CRUD над словарём акронимов (опция aui_acronyms)
│   ├── class-aui-install.php        # activation: дефолтный словарь + миграция со старого плагина
│   ├── class-aui-settings.php       # опция aui_settings + enqueue фронтенд-ассетов + CSS-переменные
│   ├── class-aui-replacer.php       # ядро: tag-aware замена текста в the_content/comment_text
│   ├── class-aui-list-table.php     # WP_List_Table для списка акронимов (только is_admin())
│   └── class-aui-admin-page.php     # страница Tools → Acronyms: форма + список + обработчики POST (только is_admin())
└── assets/
    ├── css/
    │   ├── acronym-ui.css           # фронтенд: подчёркивание, значок, бабл подсказки
    │   └── admin.css                # мелкие правки страницы Tools → Acronyms
    └── js/
        └── acronym-ui.js            # тултип: hover/focus (десктоп) + click/tap (мобильные), без jQuery

class-aui-list-table.php и class-aui-admin-page.php подключаются в acronym-ui.php только внутри if ( is_admin() ) — на фронтенде WordPress эти файлы вообще не загружаются (в том числе не тянется wp-admin/includes/class-wp-list-table.php), чтобы не тратить память на каждый публичный запрос.

Классы без PHP-неймспейса, с префиксом AUI_ (как LAGR_ в соседнем llm-agent-github-repos — единый стиль для плагинов в этом репозитории). Все классы статические (без состояния экземпляра, кроме AUI_List_Table, которая обязана быть объектом по контракту WP_List_Table) и сами регистрируют свои хуки в собственном init(), который дергается один раз из aui_boot() (plugins_loaded).

Модель данных

Всё хранится в двух опциях wp_options (без отдельных таблиц — словарь и настройки не настолько велики, чтобы это того стоило):

  • aui_acronyms — ассоциативный массив 'ACRONYM' => 'Full text'. Владелец — AUI_Repository (get_all(), get(), save(), delete(), delete_many(), search(), count()). Ключи — акронимы как они появляются в тексте (регистрозависимо, как и в оригинале).
  • aui_settings — ассоциативный массив настроек отображения (см. Настройки). Владелец — AUI_Settings (get_all(), get( $key, $default ), update( array $values )); get_all() всегда домешивает defaults() через wp_parse_args(), так что частично сохранённая опция (после апгрейда плагина с новыми ключами) не ломает чтение.
  • aui_migrated_from_legacy — служебный флаг (1), чтобы миграция со старого плагина (см. Миграция и удаление) выполнялась ровно один раз.

Алгоритм замены (AUI_Replacer::replace())

Метод — публичный статический AUI_Replacer::replace( string $html ): string, навешан на the_content и comment_text (каждый — опционально, в зависимости от настроек) с приоритетом 20, чтобы отработать после большинства других фильтров контента (шорткоды, embeds и т.п.), но до вывода.

  1. Построение паттерна (build_pattern()): ключи словаря сортируются по убыванию длины (чтобы IE6 матчился раньше IE и не оставлял висящий 6), экранируются preg_quote() и собираются в один паттерн /\b(ACR1|ACR2|…)\b/u. Строится один раз на вызов replace(), а не на каждый акроним — это и есть замена N проходов preg_replace из оригинала на 1 проход preg_replace_callback по каждому текстовому токену.
  2. Токенизация: preg_split('/(<[^>]+>)/', $html, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY) режет HTML на чередующиеся токены «тег» / «текст между тегами».
  3. Обход токенов с счётчиком вложенности $skip_depth:
    • Токен-тег: регэкспом достаётся имя тега и признак закрывающего (/tag). Если имя есть в SKIP_TAGS, $skip_depth увеличивается на открывающем и уменьшается на закрывающем теге (не уходя ниже 0). Сам токен-тег всегда выводится как есть — подстановка никогда не трогает атрибуты.
    • Токен-текст: если $skip_depth === 0 (мы сейчас не внутри «запрещённого» тега) — к тексту применяется preg_replace_callback() с собранным паттерном; иначе текст выводится без изменений.
  4. SKIP_TAGS (список тегов, внутри которых акронимы никогда не подсвечиваются, включая их потомков): a, h1–h6, abbr, script, style, code, pre, kbd, textarea, select, option, button, template, title. a и h1–h6 закрывают явное требование «не срабатывать в заголовках и ссылках»; abbr не даёт задвоить уже вручную проставленную разметку; остальные — чтобы не портить программные примеры и служебную разметку. Список фильтруется через apply_filters('aui_skip_tags', …).
  5. Подстановка одного вхождения (render_match()): если у акронима ещё не исчерпан лимит max_highlights — счётчик self::$seen_counts[$acronym] (статик-массив, живёт в рамках одного HTTP-запроса) увеличивается и возвращается <abbr class="aui-acronym" title="{fulltext}" tabindex="0">{acronym}</abbr> (оба значения экранированы esc_attr()/esc_html()); если лимит уже исчерпан — акроним возвращается как обычный текст без изменений. Итоговая разметка одного вхождения дополнительно проходит через apply_filters('aui_acronym_html', …).

Счётчик $seen_counts общий для the_content и comment_text в рамках одного запроса — поэтому лимит держится «на странице» целиком: и на архиве с несколькими постами, и на посте с комментариями под ним, а не только в пределах одного отдельного вызова фильтра.

Пример (при max_highlights = 1, словарь по умолчанию содержит LLM → Large Language Model):

<!-- вход -->
<h2>Про LLM</h2>
<p>LLM — это <a href="/wiki/llm">LLM</a>, LLM встречается тут ещё раз.</p>

<!-- выход -->
<h2>Про LLM</h2>
<p><abbr class="aui-acronym" title="Large Language Model" tabindex="0">LLM</abbr> — это
<a href="/wiki/llm">LLM</a>, LLM встречается тут ещё раз.</p>

Заголовок и текст ссылки не тронуты; из трёх оставшихся вхождений в параграфе подсвечено только первое (лимит — 1), второе и третье остаются обычным текстом несмотря на то, что они вне a/h2.

Настройки

Все настройки — под aui_settings, редактируются на Tools → Acronyms (AUI_Settings::defaults()):

Ключ По умолчанию Назначение
enable_content 1 Подключать ли фильтр the_content
enable_comments 1 Подключать ли фильтр comment_text
enable_underline 1 Пунктирное подчёркивание (body.aui-no-underline снимает его через CSS)
enable_icon 1 Значок после акронима (body.aui-no-icon скрывает через content: none)
icon_glyph [?] Текст значка, попадает в CSS-переменную --aui-icon-glyph
underline_color #666666 Цвет подчёркивания (--aui-underline-color), валидируется sanitize_hex_color()
icon_color #666666 Цвет значка (--aui-icon-color), валидируется sanitize_hex_color()
max_highlights 1 Сколько раз один акроним подсвечивается на странице; 0 — без ограничения

enable_content/enable_comments читаются один раз при AUI_Replacer::init() (на plugins_loaded) — решают, регистрировать ли сам фильтр; enable_underline/enable_icon/цвета/глиф читаются на каждый показ страницы в AUI_Settings::enqueue_frontend_assets() и превращаются в инлайн `