# NAME
Encode::Wide - Convert wide characters (Unicode, UTF-8, etc.) into ASCII-safe HTML or XML entities
# VERSION
0.07
# SYNOPSIS
use Encode::Wide qw(wide_to_html wide_to_xml);
# Basic HTML conversion
my $html = wide_to_html(string => "Cafe\x{E9} d\x{E9}j\x{E0} vu");
# => 'Café déjà vu'
# Basic XML conversion (numeric entities, en-dash folded to hyphen)
my $xml = wide_to_xml(string => "Cafe\x{E9} \x{2013} na\x{EF}ve");
# => 'Café - naïve'
# Preserve embedded HTML markup (keep_hrefs)
my $linked = wide_to_html(
string => 'Caf\x{E9}',
keep_hrefs => 1,
);
# => 'Café'
# Keep apostrophes literal for JavaScript contexts (keep_apos)
my $js_safe = wide_to_html(
string => "it\x{2019}s na\x{EF}ve",
keep_apos => 1,
);
# => "it\x{2019}s naïve" (curly apostrophe kept; i-umlaut encoded)
# Get notified about unhandled characters instead of dying silently
my $out = wide_to_html(
string => $untrusted,
complain => sub { warn "Unhandled: $_[0]" },
);
# Accept a scalar reference
my $text = "na\x{EF}ve";
my $safe = wide_to_html(string => \$text);
# => 'naïve'
# DESCRIPTION
Encode::Wide converts strings that contain non-ASCII (wide) characters into
pure 7-bit ASCII output suitable for embedding in HTML pages or XML documents.
Every non-ASCII codepoint is replaced by the appropriate entity reference so
the output can be safely placed in HTML attributes, HTML body text, or XML
element content without triggering encoding errors or security issues.
## Why use this module?
[HTML::Entities](https://metacpan.org/pod/HTML%3A%3AEntities) is the obvious alternative for HTML, but it makes strict
assumptions about input encoding that cause silent failures when the input
arrives as raw UTF-8 bytes, already-partially-encoded entities, or a mix of
both. Encode::Wide handles all three representations through a multi-pass
pipeline and falls back to [HTML::Entities](https://metacpan.org/pod/HTML%3A%3AEntities) numeric encoding for any
character not explicitly listed in its tables.
For XML, [XML::Entities](https://metacpan.org/pod/XML%3A%3AEntities) works in the opposite direction (decoding entities,
not encoding them). Encode::Wide fills that gap.
## Input
Both functions accept:
- A **Perl Unicode string** (the internal `utf8` flag is set) - the normal case
when input comes from ["decode" in Encode](https://metacpan.org/pod/Encode#decode), a database driver with `pg_enable_utf8`,
or a source file declared `use utf8`.
- A **raw UTF-8 byte string** - the common case when input arrives from a legacy
web form or an older database driver without automatic decoding. The pipeline's
raw-byte substitution pass handles this transparently.
- A **scalar reference** - `wide_to_html(string => \$var)`. The string is
read from the referent; the referent is not modified.
- **Already-encoded HTML entities** - e.g. `é` or `<`.
By default the pipeline decodes these first so they are not double-encoded.
Pass `keep_hrefs => 1` to suppress decoding when the input contains
trusted HTML that must pass through unchanged.
## Output
Both functions return a **defined scalar string** containing **only ASCII
characters** (code points 0x00-0x7F). The output is safe to concatenate
directly into an HTML or XML document without further escaping.
## Choosing between the two functions
Use `wide_to_html` when writing into an HTML context (`
`, `
`,
attribute values, etc.). Named entities such as `é` and `–`
are used wherever possible; they are compact and human-readable in the source.
Use `wide_to_xml` when writing into an XML context (XHTML, RSS, Atom, custom
XML schemas). Named HTML entities other than the five predefined XML entities
(`&` `<` `>` `'` `"`) are not valid in XML.
This function uses only hexadecimal numeric entities (`é`), which are
valid in all XML 1.0 processors. Em-dashes and en-dashes are folded to a
plain ASCII hyphen because many XML consumers normalise whitespace and
punctuation anyway.
# EXPORT
Nothing is exported by default. Import the functions you need explicitly:
use Encode::Wide qw(wide_to_html); # one function
use Encode::Wide qw(wide_to_html wide_to_xml); # both
# COMMON PARAMETERS
Both functions accept the following named parameters in addition to `string`.
Pass them as a flat key-value list:
wide_to_html(string => $text, keep_hrefs => 1, complain => \&handler);
- `string` (required)
The text to encode. May be a plain scalar or a **reference to a scalar**.
Must be defined; passing `undef` causes the function to `croak` with a
usage message.
- `keep_hrefs` (optional, default 0)
When true, angle brackets (`<`, `>`) and double-quotes (`"`) are
**not** escaped, allowing embedded HTML or XML markup to survive intact.
**Security note:** when `keep_hrefs` is set, entity-decoding is also
suppressed. Without this suppression, an encoded payload such as
`<script>` would be decoded to `` and pass through to the output
unescaped, creating a stored XSS vector.
**Fix applied in 0.07:** when `keep_hrefs` is true, the decode step is also
skipped. The pipeline treats the input as already-trusted HTML; wide
characters are still encoded, but entity normalisation becomes the caller's
responsibility.
## ReDoS in bare-ampersand substitution
The substitution that escapes bare `&` characters uses a negative lookahead
to distinguish bare ampersands from valid entity references. A naive
backtracking quantifier inside that lookahead creates O(n^2) work for inputs
such as `&aaaaa...X` (many word characters, no closing semicolon).
**Fix applied in 0.07:** the character class inside the lookahead uses a
possessive quantifier `[A-Za-z#0-9]++`, which commits matches and prevents
backtracking. Perl 5.10 or later is required, consistent with the declared
`MIN_PERL_VERSION`.
## Eval-free substitutions
All substitutions in this module use plain `/g` rather than `/ge` (evaluate
replacement as Perl code). The `/e` flag was present in earlier versions but
was unnecessary: hash lookups are value interpolation, not executable code.
Removing `/e` eliminates a class of potential code-injection issues should a
future change inadvertently expose user-controlled data in the replacement
expression.
# LIMITATIONS
- Character coverage is hand-maintained
Both functions use explicit `@byte_map` tables organised into three passes
(raw UTF-8 bytes, `\N{U+...}` named chars, literal Unicode source chars).
Characters not covered by these tables fall back to
`HTML::Entities::encode_entities_numeric` in `wide_to_html`, or trigger a
fatal `BUG:` error in `wide_to_xml` (XML has no safe generic numeric
fallback). To add a missing character, extend all three passes for the
relevant function and add a regression test in `t/30-basics.t`.
- No ` |