# 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 `