File Coverage

blib/lib/Data/Printer/Filter/EscapeNonPrintable.pm
Criterion Covered Total %
statement 31 32 96.8
branch 5 6 83.3
condition 1 3 33.3
subroutine 7 7 100.0
pod 0 1 0.0
total 44 49 89.8


line stmt bran cond sub pod time code
1             package Data::Printer::Filter::EscapeNonPrintable;
2              
3 2     2   813265 use v5.26.0;
  2         10  
4              
5 2     2   18 use strict;
  2         5  
  2         76  
6 2     2   11 use warnings;
  2         4  
  2         126  
7 2     2   16 use feature qw< signatures >;
  2         5  
  2         485  
8 2     2   17 no warnings qw< experimental::signatures >;
  2         4  
  2         88  
9              
10 2     2   830 use Data::Printer::Filter;
  2         6512  
  2         18  
11              
12             our $VERSION = 'v1.0.0';
13              
14             filter 'SCALAR' => \&parse;
15              
16             my $VT = "\x{0b}"; # ASCII vertical tab
17             my $VT_ESC = '\v'; # " (C escape)
18             my $ESC = "\x{1b}"; # ASCII ESC
19             my $DEL = "\x{7f}"; # ASCII DEL
20              
21             # DDP's missing ASCII control chars + DEL.
22             # See ascii(7).
23             my $MISSING_CHARS_RGX = qr{
24             [ \x{01}-\x{06} \x{0e}-\x{1a} \x{1c}-\x{1f} $DEL ]
25             }xx;
26              
27             my $VT_OR_MISSING_RGX = qr{
28             (?'VT' \Q$VT\E+)
29             |
30             ${MISSING_CHARS_RGX}+
31             }x;
32              
33             # ANSI reset color code.
34             # See https://en.wikipedia.org/wiki/ANSI_escape_code.
35             my $RESET_COLOR_RGX = qr{
36             $ESC \[ 0? m
37             }x;
38              
39 5         10 sub parse ( $scalar_ref, $ddp )
40 5     5 0 17798 {
  5         10  
  5         10  
41 5 50 33     28 if ( defined $scalar_ref->$* && $ddp->print_escapes ) {
42             # Only process the scalar after DDP filters it, otherwise all missing chars
43             # will be recolorized.
44 5         53 my $str = Data::Printer::Filter::SCALAR::parse( $scalar_ref, $ddp );
45              
46             # Escape the vertical tabs to '\v' and remaining missing chars to octal;
47             # colorize them when required.
48 5         1597 $str =~ s{
49             $VT_OR_MISSING_RGX
50             }
51             {
52             # Escape multiple chars in one go, e.g. \x0b\x0b\x0b
53             my $char = defined $+{VT}
54 18 100       236 ? $& =~ s{$VT}{$VT_ESC}gr # \v
55             : sprintf( ('\\%03o') x length $&, unpack( 'C*', $& ) ); # Octal
56              
57 18 100       73 if ( $ddp->colored ) {
58             # NOTE:
59             # 'string' color must be restored because this filter is injecting colors
60             # in an already colored string, or else injected colors propagate until
61             # the end of string.
62              
63             # Strip trailing reset codes.
64 13         87 my $escaped = $ddp->maybe_colorize( $char, 'escaped' ) =~ s{$RESET_COLOR_RGX\z}{}r;
65 13         460 my $restore = $ddp->maybe_colorize( '', 'string' ) =~ s{$RESET_COLOR_RGX\z}{}r;
66              
67 13         525 $escaped . $restore;
68             }
69             else {
70 5         70 $char;
71             }
72             }gex;
73              
74 5         28 return $str;
75             }
76              
77 0           return;
78             }
79              
80             =encoding UTF-8
81              
82             =for highlighter language=perl
83              
84             =head1 NAME
85              
86             Data::Printer::Filter::EscapeNonPrintable - escape missing ASCII nonprintable characters
87              
88             =head1 SYNOPSIS
89              
90             In your F<.dataprinter>:
91              
92             print_escapes = 1
93             filters = EscapeNonPrintable
94              
95             Alternatively:
96              
97             use DDP print_escapes => 1, filters => ['EscapeNonPrintable'];
98              
99             =head1 DESCRIPTION
100              
101             This module is a filter plugin for L.
102              
103             =head2 Rationale
104              
105             Since L does not escape all ASCII control chars in L,
106             this filter escapes them to octal notation. Vertical tab (C<\x0b>) and DEL (C<\x7f>)
107             chars are also missed, thus escaped.
108              
109             All the missing chars are colorized to their C L.
110              
111             Note that this is a hack; the proper way to fix this should be to patch DDP.
112              
113             See:
114              
115             =over 4
116              
117             =item *
118              
119             L
120              
121             =item *
122              
123             L
124              
125             =item *
126              
127             L
128              
129             =back
130              
131             =head1 DECORATING
132              
133             To call this filter and process its result from another filter, you can use:
134              
135             my $str = Data::Printer::Filter::EscapeNonPrintable::parse( $ref, $ddp );
136              
137             Note that the loading order of filters matters, so ensure the caller is the last one in the chain.
138              
139             =head1 BUGS
140              
141             Report bugs at L.
142              
143             =head1 AUTHOR
144              
145             ryoskzypu
146              
147             =head1 SEE ALSO
148              
149             =over 4
150              
151             =item *
152              
153             L
154              
155             =item *
156              
157             L
158              
159             =item *
160              
161             L
162              
163             =item *
164              
165             L
166              
167             =item *
168              
169             L
170              
171             =back
172              
173             =head1 COPYRIGHT
174              
175             Copyright © 2026 ryoskzypu
176              
177             MIT-0 License. See LICENSE for details.
178              
179             =cut
180              
181             1;