NAME
HTML::Anchor::Audit - offline, source-located diagnostics for HTML fragment links
SYNOPSIS
use utf8;
use HTML::Anchor::Audit qw(audit_html audit_utf8);
my $report = audit_html(q{
<a href="#installation">Install</a>
<h2 id="installation">Installation</h2>
<a href="#missing">Missing section</a>
});
for my $issue (@{ $report->{issues} }) {
printf "%s at line %d, column %d\n",
$issue->{code}, $issue->{line}, $issue->{column};
}
# Read a local UTF-8 article without modifying it.
open my $fh, '<:raw', 'article.html' or die $!;
my $bytes = do { local $/; <$fh> };
my $utf8_report = audit_utf8($bytes);
DESCRIPTION
This small library checks fragment-only links in already available HTML. It returns Perl data structures for editor integrations, pre-publication checks and static-content pipelines. It does not fetch URLs, open files, modify content or send telemetry. The bundled html-anchor-audit command reads local files.
The use case is a saved article or HTML fragment, including Gutenberg exports, where section buttons may outlive their targets. The result includes missing targets, repeated IDs and references to multiple matching targets, with source locations usable by an editor. It is independent of WordPress and does not need access to a WordPress installation.
FUNCTIONS
Nothing is exported by default. Import either function explicitly.
audit_html($characters, %options)
Accepts a decoded Perl character string. Decode non-ASCII input before calling; the function cannot infer whether an unflagged scalar represents bytes or text. It returns a fresh hash reference and leaves the supplied string unchanged.
audit_utf8($bytes, %options)
Accepts UTF-8 octets and rejects invalid UTF-8. A leading UTF-8 BOM is retained for source-location accounting. Files in other encodings must be decoded by the caller, then supplied to audit_html.
Options
max_bytes defaults to 2097152, measured after encoding characters to UTF-8. Larger input throws an exception. max_issues defaults to 500; a larger diagnostic count sets statistics.truncated and retains the first diagnostics discovered, which are then sorted by location. Both options require positive integers. Targets and references are still collected completely within the input size limit. Unknown options and invalid arguments throw exceptions.
REPORT FORMAT
The schema_version is 1. targets, references and issues are arrays of hash references. Locations identify the start of the opening tag, rather than the attribute. line and column are one-based; the column counts Unicode characters, not screen cells. byte_column is one-based and byte_offset is zero-based in the UTF-8 source. CR, LF and CRLF are supported.
Targets contain kind (id or legacy name), value and a location. References contain href, fragment, a location and status: matched, ambiguous, missing, top or ignored. Resolved references include target_kind, target_value, target_count and targets. To bound reports for pathological duplicate IDs, per-reference targets and ambiguity related list at most the first ten locations; target_count and related_count retain the full counts. The top-level targets remains complete. Ignored references have reason base_href or fragment_directive.
Issue code is missing_target, duplicate_id or ambiguous_target. The value names the relevant target. related lists target locations for duplicates and ambiguities. A missing target can include normalization_candidates: existing values equivalent under Unicode NFC. These are hints only; Unicode normalization never turns a missing target into a match. A repeated legacy name is reported when a link resolves ambiguously, not as a duplicate ID. Multiple targets warn even though a browser may choose the first one.
statistics contains target/reference counts, checked/ignored reference counts, out_of_scope_href_count, full issue_count, reported_issue_count and integer truncated (0 or 1).
MATCHING RULES AND LIMITS
HTML::Parser tokenizes the source and decodes attribute entities. IDs are case-sensitive. Only a and area fragment-only href values are checked. Tabs and line breaks are removed from URLs and surrounding ASCII controls and spaces are trimmed. An empty fragment points to the top. Exact fragment matching is tried before a single percent-decoding pass followed by UTF-8 decoding, with replacement for invalid encoded sequences. IDs take priority over legacy a name targets in each pass. Literal + stays +. An otherwise unmatched ASCII case-insensitive top fragment points to the top.
Template content is ignored; the template element's own ID remains visible. Comment and raw-text contents are handled by HTML::Parser. Fragment directives such as #:~:text=... are ignored. If the first base href is nonempty, all fragment references are ignored because they may target another document.
The checker does not reconstruct an HTML5 DOM. Malformed nesting, duplicate attributes, embedded SVG/XML, scripting-dependent noscript content and HTML5 entity edge cases may differ from a browser. Use well-formed static HTML. IDs produced by JavaScript, theme wrappers or a later renderer are not known. Other documents, external URLs, CSS selectors and Gutenberg block balancing are out of scope. This is not a complete HTML validator or a web crawler.
COMMAND LINE
html-anchor-audit article.html
html-anchor-audit --json article.html second-article.html
html-anchor-audit --max-bytes 4194304 article.html
Input is strictly UTF-8. Exit status is 0 for a complete clean check, 1 if diagnostics were found, and 2 for a usage, file or input error. JSON output is a single UTF-8 object with version and files; each file entry has file and either report or error. Output does not include the full article. The command has no URL or network mode and never writes the input files.
SIMILAR TOOLS
HTML::SimpleLinkExtor extracts links. HTML::Lint checks broader HTML errors. WWW::LinkRot checks remote link status. The W3C Link Checker already checks duplicate anchors and fragments, with crawling and network features. Prefer it for site-wide validation. This module offers a narrow, offline, embeddable API with structured source locations and Unicode hints; it does not claim to invent fragment checking.
Matching is informed by the HTML Standard fragment-selection algorithm, within the static-source limits above.
SUPPORT
Report reproducible issues at the project issue tracker.
AUTHOR
Nedal Shabaan. Developed for article-editing workflows at Idraaak.
LICENSE
Copyright (c) 2026 Nedal Shabaan. Released under the MIT license; see LICENSE.