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   9084 use strict;
  32         75  
  32         8630  
4 32     32   208 use warnings;
  32         84  
  32         6486  
5 32     32   206 use utf8;
  32         64  
  32         258  
6 32     32   1255 use feature ':5.24';
  32         61  
  32         10987  
7              
8 32     32   241 use Carp;
  32         79  
  32         11396  
9 32     32   749 use English;
  32         2679  
  32         261  
10 32     32   52375 use Exporter 'import';
  32         83  
  32         1495  
11 32     32   682 use List::MoreUtils 'first_index';
  32         12211  
  32         346  
12 32     32   80647 use List::Util 'max', 'min';
  32         78  
  32         4206  
13 32     32   47524 use Unicode::CaseFold 'fc';
  32         105756  
  32         149601  
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 754099 my $test = shift;
25 2     7   69 my $i = first_index { !$test->($_) } @_;
  7         34  
26 2 100       28 return (\@_, []) if $i < 0;
27 1         5 my @pass = splice(@_, 0, $i);
28 1         17 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 54818     54818 0 159846 my ($n, $text, $preserve_tabs) = @_;
38 54818   100     301040 $preserve_tabs //= 1; # when not specified we do preserve tabs
39 54818 100       175620 if (!$preserve_tabs) {
40 22         76 my $s = indent_size($text); # this sets pos($text);
41 22         195 my $ret = (' ' x max(0, $s - $n)).(substr $text, pos($text));
42 22 50       220 return $ret unless wantarray;
43 0         0 return ($ret, min($s, $n));
44             }
45 54796         131772 my $t = int($n / 4);
46 54796         113539 my $s = $n % 4;
47 54796         94452 my $m = 0; # How many spaces we have matched.
48 54796         135207 for my $i (1 .. $t) {
49 2450 100       15303 if ($text =~ m/^( {0,3}\t| {4})/) {
50             # We remove one full tab-stop from the string.
51 2288         8701 substr $text, 0, length($1), '';
52 2288         14320 $m += 4;
53             } else {
54             # We didn’t have a full tab-stop, so we remove as many spaces as we had.
55 162 50       739 $text =~ m/^( {0,3})/ or confess 'Unexpected match failure';
56 162         853 $m += $LAST_MATCH_END[0] - $LAST_MATCH_START[0];
57 162 100       1357 return substr $text, length($1) unless wantarray;
58 37         280 return ((substr $text, length($1)), $m);
59             }
60             }
61 54634 100       181908 if ($s != 0) {
62 30857         214128 $text =~ m/^(?

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

63 30857         270520 my $ns = length $+{s};
64 30857 100       155533 if ($ns >= $s) {
    100          
65 16937   100     246439 $text = ($+{p} // '').($+{l} // '').(' ' x ($ns - $s)).$+{e};
      100        
66 16937         71359 $m += $s;
67             } elsif (length($+{l})) {
68 877   100     10405 $text = ($+{p} // '').(' ' x (4 + $ns - $s)).$+{e};
69 877         2826 $m += $s;
70             } else {
71 13043         66786 $text = $+{e};
72 13043         31383 $m += $ns;
73             }
74             }
75 54634 100       344151 return $text unless wantarray;
76 14019         62743 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 49029     49029 0 222750 pos($_[0]) = 0;
89 49029         264806 my $t = () = $_[0] =~ m/\G( {0,3}\t| {4})/gc; # Forcing list context.
90 49029         182742 $_[0] =~ m/\G( *)/g;
91 49029         139792 my $s = length($1); ## no critic (ProhibitCaptureWithoutTest)
92 49029 100 100     214214 if (substr($_[0], 0, 1) eq "\t" && @_ > 1) {
93 41         182 $s -= $_[1] % 4;
94             }
95 49029         208048 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 10 my ($text) = @_;
102 5         30 my $t = () = $text =~ m/\G([^\t]{0,3}\t|[^\t]{4})/gc; # Forcing list context.
103 5   100     16 my $s = length($text) - (pos($text) // 0);
104 5         19 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 9850 return indented(4, $_[0]);
110             }
111              
112             sub indented {
113 16478     16478 0 72648 my ($n, $text) = @_;
114 16478         51889 my $t = int($n / 4);
115 16478         37408 my $s = $n % 4;
116 16478         53038 for my $i (1 .. $t) {
117 16471 100       156897 return unless $text =~ m/\G(?: {0,3}\t| {4})/g;
118             }
119 896 100       9838 return 1 if $text =~ m/\G(?: {$s}| *\t)/;
120 586         2842 return;
121             }
122              
123             # Performs the normalization described in:
124             # https://spec.commonmark.org/0.31.2/#matches
125             sub normalize_label {
126 19896     19896 0 60045 my ($label) = @_;
127 19896   50     118231 $label = fc($label) // ''; # fc returns undef for empty label.
128 19896         348877 $label =~ s/^[ \t\n]+|[ \t\n]+$//g;
129 19896         102724 $label =~ s/[ \t\n]+|[\t\n]/ /g;
130 19896         65131 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 224     224 0 1261 my ($str, $prefix) = @_;
137 224   100     751 $prefix //= 0;
138 224         1386 while ($str =~ m/\G[^\t]*\t/g) {
139 119         632 $prefix += $LAST_MATCH_END[0] - $LAST_MATCH_START[0] - 1;
140 119         395 my $nb_space = 4 - $prefix % 4;
141 119         635 substr $str, $LAST_MATCH_END[0] - 1, 1, ' ' x $nb_space;
142 119         493 pos($str) = $LAST_MATCH_END[0] - 1 + $nb_space;
143 119         516 $prefix = 0; # By definition we are now aligned with a tab stop.
144             }
145 224 100       921 return $str if defined wantarray;
146 220         578 $_[0] = $str;
147 220         576 return;
148             }
149              
150             1;