File Coverage

blib/lib/Markdown/Perl/Util.pm
Criterion Covered Total %
statement 100 101 99.0
branch 26 28 92.8
condition 16 17 94.1
subroutine 19 19 100.0
pod 0 8 0.0
total 161 173 93.0


line stmt bran cond sub pod time code
1             package Markdown::Perl::Util;
2              
3 32     32   6791 use strict;
  32         58  
  32         1267  
4 32     32   147 use warnings;
  32         52  
  32         1477  
5 32     32   147 use utf8;
  32         48  
  32         192  
6 32     32   1040 use feature ':5.24';
  32         51  
  32         4055  
7              
8 32     32   230 use Carp;
  32         52  
  32         3284  
9 32     32   757 use English;
  32         2958  
  32         164  
10 32     32   11997 use Exporter 'import';
  32         65  
  32         1164  
11 32     32   964 use List::MoreUtils 'first_index';
  32         19771  
  32         273  
12 32     32   22533 use List::Util 'max', 'min';
  32         60  
  32         3731  
13 32     32   17594 use Unicode::CaseFold 'fc';
  32         37090  
  32         71767  
14              
15             our $VERSION = 0.01;
16              
17             our @EXPORT_OK =
18             qw(split_while remove_prefix_spaces indent_size indented_one_tab horizontal_size normalize_label indented tabs_to_space);
19             our %EXPORT_TAGS = (all => \@EXPORT_OK);
20              
21             # Partition a list into a continuous chunk for which the given code evaluates to
22             # true, and the rest of the list. Returns a list of two array-ref.
23             sub split_while : prototype(&@) { ## no critic (RequireArgUnpacking)
24 2     2 0 509853 my $test = shift;
25 2     7   54 my $i = first_index { !$test->($_) } @_;
  7         32  
26 2 100       29 return (\@_, []) if $i < 0;
27 1         6 my @pass = splice(@_, 0, $i);
28 1         14 return (\@pass, \@_);
29             }
30              
31             # Removes the equivalent of n spaces at the beginning of the line. Tabs are
32             # matched to a tab-stop of size 4.
33             # Removes all the spaces if there is less than that.
34             # If needed, tabs are converted into 4 spaces.
35             # In list context, also returns how many spaces were actually matched.
36             sub remove_prefix_spaces {
37 53334     53334 0 84467 my ($n, $text, $preserve_tabs) = @_;
38 53334   100     164977 $preserve_tabs //= 1; # when not specified we do preserve tabs
39 53334 100       79299 if (!$preserve_tabs) {
40 45         153 my $s = indent_size($text); # this sets pos($text);
41 45         296 my $ret = (' ' x max(0, $s - $n)).(substr $text, pos($text));
42 45 50       314 return $ret unless wantarray;
43 0         0 return ($ret, min($s, $n));
44             }
45 53289         81168 my $t = int($n / 4);
46 53289         61996 my $s = $n % 4;
47 53289         52765 my $m = 0; # How many spaces we have matched.
48 53289         79566 for my $i (1 .. $t) {
49 2479 100       9083 if ($text =~ m/^( {0,3}\t| {4})/) {
50             # We remove one full tab-stop from the string.
51 2251         5321 substr $text, 0, length($1), '';
52 2251         3765 $m += 4;
53             } else {
54             # We didn’t have a full tab-stop, so we remove as many spaces as we had.
55 228 50       663 $text =~ m/^( {0,3})/ or confess 'Unexpected match failure';
56 228         758 $m += $LAST_MATCH_END[0] - $LAST_MATCH_START[0];
57 228 100       1164 return substr $text, length($1) unless wantarray;
58 34         149 return ((substr $text, length($1)), $m);
59             }
60             }
61 53061 100       72915 if ($s != 0) {
62 30567         118160 $text =~ m/^(?

\ {0,3}\t|\ {4})*?(?\ {0,3}\t|\ {4})?(?\ {0,3})(?[^ \t].*|$)/xs; ## no critic (ProhibitComplexRegexes)

63 30567         133771 my $ns = length $+{s};
64 30567 100       79428 if ($ns >= $s) {
    100          
65 16930   100     125320 $text = ($+{p} // '').($+{l} // '').(' ' x ($ns - $s)).$+{e};
      100        
66 16930         31820 $m += $s;
67             } elsif (length($+{l})) {
68 911   100     7006 $text = ($+{p} // '').(' ' x (4 + $ns - $s)).$+{e};
69 911         1924 $m += $s;
70             } else {
71 12726         33949 $text = $+{e};
72 12726         19530 $m += $ns;
73             }
74             }
75 53061 100       130562 return $text unless wantarray;
76 13874         34631 return ($text, $m);
77             }
78              
79             # Return the indentation of the given text
80             # indent_size($str, $prev_indent)
81             #
82             # Sets pos($_[0]) to the first non-whitespace character.
83             # $prev_indent can be passed if the $str is not the beginning of the logical
84             # line, to properly compute the tab stops.
85             # TODO: this feature is used when parsing list_items, but could be used in many
86             # other places too.
87             sub indent_size { ## no critic (RequireArgUnpacking)
88 48355     48355 0 101069 pos($_[0]) = 0;
89 48355         141067 my $t = () = $_[0] =~ m/\G( {0,3}\t| {4})/gc; # Forcing list context.
90 48355         104503 $_[0] =~ m/\G( *)/g;
91 48355         67919 my $s = length($1); ## no critic (ProhibitCaptureWithoutTest)
92 48355 100 100     117421 if (substr($_[0], 0, 1) eq "\t" && @_ > 1) {
93 75         207 $s -= $_[1] % 4;
94             }
95 48355         100347 return $t * 4 + $s;
96             }
97              
98             # Compute the horizontal size of a given string (similar to indent_size, but
99             # all characters count, not just tabs and space).
100             sub horizontal_size {
101 5     5 0 14 my ($text) = @_;
102 5         44 my $t = () = $text =~ m/\G([^\t]{0,3}\t|[^\t]{4})/gc; # Forcing list context.
103 5   100     24 my $s = length($text) - (pos($text) // 0);
104 5         27 return $t * 4 + $s;
105             }
106              
107             # Returns true if the text is indented by at least one tab-stop.
108             sub indented_one_tab {
109 6     6 0 3206 return indented(4, $_[0]);
110             }
111              
112             sub indented {
113 16392     16392 0 29816 my ($n, $text) = @_;
114 16392         32757 my $t = int($n / 4);
115 16392         22994 my $s = $n % 4;
116 16392         30086 for my $i (1 .. $t) {
117 16385 100       64706 return unless $text =~ m/\G(?: {0,3}\t| {4})/g;
118             }
119 818 100       4541 return 1 if $text =~ m/\G(?: {$s}| *\t)/;
120 514         1347 return;
121             }
122              
123             # Performs the normalization described in:
124             # https://spec.commonmark.org/0.31.2/#matches
125             sub normalize_label {
126 19931     19931 0 34938 my ($label) = @_;
127 19931   50     46483 $label = fc($label) // ''; # fc returns undef for empty label.
128 19931         203839 $label =~ s/^[ \t\n]+|[ \t\n]+$//g;
129 19931         58089 $label =~ s/[ \t\n]+|[\t\n]/ /g;
130 19931         34610 return $label;
131             }
132              
133             # Convert tabs to space in the given string. Assuming $prefix horizontal spaces
134             # before the string.
135             sub tabs_to_space { ## no critic (RequireArgUnpacking)
136 245     245 0 1086 my ($str, $prefix) = @_;
137 245   100     650 $prefix //= 0;
138 245         1131 while ($str =~ m/\G[^\t]*\t/g) {
139 131         559 $prefix += $LAST_MATCH_END[0] - $LAST_MATCH_START[0] - 1;
140 131         409 my $nb_space = 4 - $prefix % 4;
141 131         505 substr $str, $LAST_MATCH_END[0] - 1, 1, ' ' x $nb_space;
142 131         415 pos($str) = $LAST_MATCH_END[0] - 1 + $nb_space;
143 131         367 $prefix = 0; # By definition we are now aligned with a tab stop.
144             }
145 245 100       587 return $str if defined wantarray;
146 241         550 $_[0] = $str;
147 241         499 return;
148             }
149              
150             1;