From 7df975dcfae1ac77722815a5ed7e2d027755d829 Mon Sep 17 00:00:00 2001 From: Dmitry Prikotov Date: Mon, 21 Sep 2026 23:26:46 +0700 Subject: [PATCH 1/3] =?UTF-8?q?fix(md-links):=20recognize=20explicit=20HTM?= =?UTF-8?q?L=20anchors=20/=20=D1=80=D0=B0=D1=81=D0=BF=D0=BE=D0=B7=D0=BD?= =?UTF-8?q?=D0=B0=D0=B2=D0=B0=D1=82=D1=8C=20=D1=8F=D0=B2=D0=BD=D1=8B=D0=B5?= =?UTF-8?q?=20HTML-=D1=8F=D0=BA=D0=BE=D1=80=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/validate-md-links | 22 +++++ docs/conventions/ops/validate-md-links.md | 1 + .../MdLinksValidator/MdLinksValidatorTest.php | 16 +++- tests/fixtures/md-links/root.md | 14 +++ tests/fixtures/md-links/subdir/target.md | 3 + todo/TASK-fix-md-links-html-anchors.todo.md | 91 +++++++++++++++++++ 6 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 todo/TASK-fix-md-links-html-anchors.todo.md diff --git a/bin/validate-md-links b/bin/validate-md-links index bd5f275d..4139beea 100755 --- a/bin/validate-md-links +++ b/bin/validate-md-links @@ -9,6 +9,8 @@ declare(strict_types=1); * Checks that internal links (relative paths and anchors) in Markdown files * point to existing files and valid anchors. External URLs, mailto: and * images are skipped. Links inside fenced code blocks are ignored. + * Valid anchor targets are ATX headings (# ...) and explicit HTML anchors + * ( / ), both GitHub-compatible. * * Usage: * php bin/validate-md-links.php [options] @@ -265,6 +267,21 @@ function generateSlug(string $heading): string return $slug ?? ''; } +/** + * Extract explicit HTML anchor ids from a line: or . + * GitHub honours both forms as link targets, so the validator must too. + * + * @return list + */ +function extractHtmlAnchors(string $line): array +{ + if (!preg_match_all('/]*?\b(?:id|name)\s*=\s*(["\'])([^"\'>]+)\1/i', $line, $m)) { + return []; + } + + return array_values(array_filter($m[2])); +} + /** * Build an anchor index from file content. * Returns map: [slug => true] (all anchors including deduplicated ones). @@ -281,6 +298,11 @@ function buildAnchorIndex(string $content): array $slugCounts = []; foreach (explode("\n", $content) as $line) { + // Explicit HTML anchors: / , used as-is + foreach (extractHtmlAnchors($line) as $id) { + $anchors[$id] = true; + } + // Match ATX headings: # ... ###### if (!preg_match('/^#{1,6}\s+(.+)$/', $line, $m)) { continue; diff --git a/docs/conventions/ops/validate-md-links.md b/docs/conventions/ops/validate-md-links.md index 1535ce87..c64d0d78 100644 --- a/docs/conventions/ops/validate-md-links.md +++ b/docs/conventions/ops/validate-md-links.md @@ -15,6 +15,7 @@ description: Настройка и использование валидатор - Внешние URL (`https://`, `mailto:`), изображения и ссылки внутри `fenced code blocks` **игнорируются**. - Валидатор поддерживает `inline`-ссылки, `reference-style`-ссылки и якоря (`anchors`). - Генерация `slug` якоря совместима с GitHub: нижний регистр (`lower-case`), русские символы, дубликаты с суффиксами `-1`, `-2`. +- Целями якорей считаются заголовки ATX (`#`…`######`) и явные HTML-якоря `` / `` — обе формы работают на GitHub и принимаются как есть, без преобразования `slug`. ## Что проверяется diff --git a/tests/MdLinksValidator/MdLinksValidatorTest.php b/tests/MdLinksValidator/MdLinksValidatorTest.php index 2b8dc6b9..ddbf76a0 100644 --- a/tests/MdLinksValidator/MdLinksValidatorTest.php +++ b/tests/MdLinksValidator/MdLinksValidatorTest.php @@ -118,9 +118,23 @@ public function testValidReferenceLinks(): void $this->assertStringNotContainsString('second-id', self::$fixtureOutput); } + public function testRecognizesExplicitHtmlAnchors(): void + { + // Explicit / anchors are GitHub-valid targets + $this->assertStringNotContainsString('#explicit-term', self::$fixtureOutput); + $this->assertStringNotContainsString('#shared-term', self::$fixtureOutput); + $this->assertStringNotContainsString('#legacy-anchor', self::$fixtureOutput); + } + + public function testDetectsBrokenExplicitAnchor(): void + { + $this->assertStringContainsString('root.md', self::$fixtureOutput); + $this->assertStringContainsString('anchor not found: #no-such-explicit', self::$fixtureOutput); + } + public function testReportsCorrectErrorCount(): void { - $this->assertStringContainsString('Found 5 broken link(s)', self::$fixtureOutput); + $this->assertStringContainsString('Found 6 broken link(s)', self::$fixtureOutput); } public function testNoFailOptionExitsZero(): void diff --git a/tests/fixtures/md-links/root.md b/tests/fixtures/md-links/root.md index ea805976..4a9413a1 100644 --- a/tests/fixtures/md-links/root.md +++ b/tests/fixtures/md-links/root.md @@ -42,3 +42,17 @@ More content. A link to [duplicate heading](#duplicate-heading-1). A link to [russian anchor](subdir/target.md#русский-заголовок). + +## Explicit HTML anchors + + +**Explicit term** — definition with an explicit anchor. + +A [local explicit anchor](#explicit-term) in this file. + +A [cross-file explicit anchor](subdir/target.md#shared-term). + + +A [legacy name anchor](#legacy-anchor). + +A [broken explicit anchor](#no-such-explicit). diff --git a/tests/fixtures/md-links/subdir/target.md b/tests/fixtures/md-links/subdir/target.md index 4377ec05..4f3f76b5 100644 --- a/tests/fixtures/md-links/subdir/target.md +++ b/tests/fixtures/md-links/subdir/target.md @@ -1,5 +1,8 @@ # Target File + +**Shared term** — target for a cross-file explicit anchor. + ## Section One Target content. diff --git a/todo/TASK-fix-md-links-html-anchors.todo.md b/todo/TASK-fix-md-links-html-anchors.todo.md new file mode 100644 index 00000000..5a728a83 --- /dev/null +++ b/todo/TASK-fix-md-links-html-anchors.todo.md @@ -0,0 +1,91 @@ +--- +type: fix +created: 2026-09-21 23:25:00 (1790007900) +due: +started: 2026-09-21 23:25:30 (1790007930) +completed: +cancelled: +value: V2 +complexity: C1 +priority: P2 +cost_plan: +cost_fact: +depends_on: +epic: +author: Бэкендер Тони (pi) +assignee: Бэкендер Тони (pi) +branch: task/md-links-html-anchors +pr: +status: in_progress +--- + +# TASK-fix-md-links-html-anchors: validate-md-links — распознавать явные HTML-якоря + +## 0. Простое описание (Human Brief) + +### Проблема простыми словами (Problem) +- `validate-md-links` строит индекс якорей только из заголовков ATX (`#`…`######`). +- Пакет `prikotov/git-workflow` использует в глоссарии явные HTML-якоря `` (валидные на GitHub): валидатор ложно помечает ссылки `#deployment`, `glossary.md#release-publishing` как битые. + +### Варианты или путь решения (Solution Sketch) +- Дополнить `buildAnchorIndex()` сбором явных HTML-якорей `` / `` (принимаются как есть, без slug-преобразования — так же, как их разрешает GitHub). +- Покрыть фикстурами и тестами, обновить документацию инструмента. + +### Ожидаемый результат (Expected Result) +- Ссылки на явные HTML-якоря валидируются корректно; документация `prikotov/git-workflow` проходит проверку без ложных срабатываний. + +## 1. Концепция и Цель (Concept and Goal) + +### История (User Story) +> **User Story:** Как потребитель пакета, я хочу, чтобы `validate-md-links` признавал явные HTML-якоря ``/`` целями ссылок — как это делает GitHub, — чтобы документация с глоссариями не падала в проверках из-за ложных `broken-anchor`. + +### Цель по SMART (Goal) +- В `bin/validate-md-links` индекс якорей собирает и заголовки, и явные HTML-якоря; добавлены фикстуры/тесты (валидные и битые явные якоря); обновлена дока `docs/conventions/ops/validate-md-links.md`; `composer check` зелёный. + +## 2. Контекст и Границы (Context and Scope) +* **Где делаем:** `bin/validate-md-links`, `tests/fixtures/md-links/`, `tests/MdLinksValidator/MdLinksValidatorTest.php`, `docs/conventions/ops/validate-md-links.md`. +* **Границы (Out of Scope):** не менять slug-генерацию заголовков; не поддерживать прочие HTML-конструкции (кроме ``/``); не трогать остальные инструменты пакета. + +## 3. Требования, MoSCoW (Requirements) +### 🔴 Обязательно (Must Have) +- [x] `buildAnchorIndex()` собирает явные HTML-якоря `` / `` (регистронезависимо, кавычки обоих видов, якорь принимается как есть). +- [x] Фикстуры: валидные локальный/межфайловый/legacy `name`-якорь и битый явный якорь. +- [x] Тесты: распознавание валидных, детект битого, счётчик ошибок обновлён. +- [x] Документация инструмента описывает поддержку HTML-якорей. +### ⚫ Won't Have (Не будем делать) +- Поддержку многострочных HTML-якорей и якорей внутри `inline code` (те же ограничения, что и у заголовков). + +## 4. План реализации (Implementation Plan) +1. [x] Добавить `extractHtmlAnchors()` и включить его в `buildAnchorIndex()`. +2. [x] Расширить фикстуры и тесты. +3. [x] Обновить документацию. +4. [x] Прогнать `composer check` и PHPUnit; сверить на документации `prikotov/git-workflow`. + +## 5. Критерии приёмки (Definition of Done) +- [x] `vendor/bin/phpunit tests/MdLinksValidator/` — зелёный (21 тест). +- [x] `composer check` — зелёный (289 тестов, PHPStan, phpcs, validate-docs, language). +- [x] `php bin/validate-md-links /path/to/git-workflow/docs/git-workflow/` — все ссылки валидны (исходный кейс закрыт). + +## 6. Самопроверка (Verification) +```bash +vendor/bin/phpunit +composer check +php bin/validate-md-links /docs/git-workflow/ +``` + +## 7. Риски и зависимости (Risks и Dependencies) +- Поведение совместимо: индекс только расширяется (новые цели), ложных «битых» меньше, новые ошибки возможны только там, где ссылки вели на несуществующие явные якоря — это корректное срабатывание. + +## 8. Источники (Sources) +- `bin/validate-md-links` — `buildAnchorIndex()`. +- Документация `prikotov/git-workflow` (`docs/git-workflow/glossary.md`) — исходный кейс с ``. + +## 9. Комментарии (Comments) +- Обнаружено при обновлении `prikotov/git-workflow` до v0.4.0 в проекте `prikotov/task-orchestrator` (PR #404). + +## История изменений (Change History) +| Дата | Автор (роль) | Изменение | +| :--- | :--- | :--- | +| 2026-09-21 23:25:00 (1790007900) | Бэкендер Тони (pi) | Создание задачи | +| 2026-09-21 23:26:00 (1790007960) | Бэкендер Тони (pi) | Старт задачи, заполнение постановки | +| 2026-09-21 23:30:00 (1790008200) | Бэкендер Тони (pi) | Реализация, фикстуры, тесты, документация; проверки зелёные | From 262ba39787e8f0f9479082839316737407529a4b Mon Sep 17 00:00:00 2001 From: Dmitry Prikotov Date: Mon, 21 Sep 2026 23:27:23 +0700 Subject: [PATCH 2/3] =?UTF-8?q?chore(todo):=20set=20PR=20link=20for=20TASK?= =?UTF-8?q?-fix-md-links-html-anchors=20/=20=D0=BF=D1=80=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D0=B2=D0=B8=D1=82=D1=8C=20=D1=81=D1=81=D1=8B=D0=BB=D0=BA?= =?UTF-8?q?=D1=83=20=D0=BD=D0=B0=20PR=20=D0=B2=20=D0=B7=D0=B0=D0=B4=D0=B0?= =?UTF-8?q?=D1=87=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- todo/TASK-fix-md-links-html-anchors.todo.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/todo/TASK-fix-md-links-html-anchors.todo.md b/todo/TASK-fix-md-links-html-anchors.todo.md index 5a728a83..4977ed19 100644 --- a/todo/TASK-fix-md-links-html-anchors.todo.md +++ b/todo/TASK-fix-md-links-html-anchors.todo.md @@ -15,8 +15,8 @@ epic: author: Бэкендер Тони (pi) assignee: Бэкендер Тони (pi) branch: task/md-links-html-anchors -pr: -status: in_progress +pr: https://github.com/prikotov/coding-standard/pull/126 +status: review --- # TASK-fix-md-links-html-anchors: validate-md-links — распознавать явные HTML-якоря From dc97db680062f122a296f639abe313c4da181488 Mon Sep 17 00:00:00 2001 From: Dmitry Prikotov Date: Mon, 21 Sep 2026 23:28:27 +0700 Subject: [PATCH 3/3] =?UTF-8?q?docs(todo):=20mark=20TASK-fix-md-links-html?= =?UTF-8?q?-anchors=20done=20/=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B2=D0=B5?= =?UTF-8?q?=D1=81=D1=82=D0=B8=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87=D1=83=20?= =?UTF-8?q?=D0=B2=20done?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- todo/{ => done}/TASK-fix-md-links-html-anchors.todo.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) rename todo/{ => done}/TASK-fix-md-links-html-anchors.todo.md (96%) diff --git a/todo/TASK-fix-md-links-html-anchors.todo.md b/todo/done/TASK-fix-md-links-html-anchors.todo.md similarity index 96% rename from todo/TASK-fix-md-links-html-anchors.todo.md rename to todo/done/TASK-fix-md-links-html-anchors.todo.md index 4977ed19..39af9f13 100644 --- a/todo/TASK-fix-md-links-html-anchors.todo.md +++ b/todo/done/TASK-fix-md-links-html-anchors.todo.md @@ -3,7 +3,7 @@ type: fix created: 2026-09-21 23:25:00 (1790007900) due: started: 2026-09-21 23:25:30 (1790007930) -completed: +completed: 2026-09-21 16:28:27 (1790008107) cancelled: value: V2 complexity: C1 @@ -16,7 +16,7 @@ author: Бэкендер Тони (pi) assignee: Бэкендер Тони (pi) branch: task/md-links-html-anchors pr: https://github.com/prikotov/coding-standard/pull/126 -status: review +status: done --- # TASK-fix-md-links-html-anchors: validate-md-links — распознавать явные HTML-якоря @@ -89,3 +89,4 @@ php bin/validate-md-links /docs/git-workflow/ | 2026-09-21 23:25:00 (1790007900) | Бэкендер Тони (pi) | Создание задачи | | 2026-09-21 23:26:00 (1790007960) | Бэкендер Тони (pi) | Старт задачи, заполнение постановки | | 2026-09-21 23:30:00 (1790008200) | Бэкендер Тони (pi) | Реализация, фикстуры, тесты, документация; проверки зелёные | +| 2026-09-21 23:40:00 (1790008800) | Бэкендер Тони (pi) | PR #126 создан, CI зелёный, задача закрыта |