line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package Dancer2::Plugin::MarkdownFilesToHTML ; |
2
|
|
|
|
|
|
|
$Dancer2::Plugin::MarkdownFilesToHTML::VERSION = '0.015'; |
3
|
2
|
|
|
2
|
|
977560
|
use 5.010; use strict; use warnings; |
|
2
|
|
|
2
|
|
18
|
|
|
2
|
|
|
2
|
|
11
|
|
|
2
|
|
|
|
|
4
|
|
|
2
|
|
|
|
|
50
|
|
|
2
|
|
|
|
|
19
|
|
|
2
|
|
|
|
|
5
|
|
|
2
|
|
|
|
|
61
|
|
4
|
|
|
|
|
|
|
|
5
|
2
|
|
|
2
|
|
12
|
use Carp; |
|
2
|
|
|
|
|
6
|
|
|
2
|
|
|
|
|
138
|
|
6
|
2
|
|
|
2
|
|
565
|
use Encode qw( decode ); |
|
2
|
|
|
|
|
9963
|
|
|
2
|
|
|
|
|
107
|
|
7
|
2
|
|
|
2
|
|
593
|
use Storable; |
|
2
|
|
|
|
|
3073
|
|
|
2
|
|
|
|
|
135
|
|
8
|
2
|
|
|
2
|
|
15
|
use File::Path qw(make_path); |
|
2
|
|
|
|
|
5
|
|
|
2
|
|
|
|
|
163
|
|
9
|
2
|
|
|
2
|
|
611
|
use Data::Dumper 'Dumper'; |
|
2
|
|
|
|
|
6061
|
|
|
2
|
|
|
|
|
108
|
|
10
|
2
|
|
|
2
|
|
15
|
use File::Basename; |
|
2
|
|
|
|
|
14
|
|
|
2
|
|
|
|
|
162
|
|
11
|
2
|
|
|
2
|
|
1117
|
use Dancer2::Plugin; |
|
2
|
|
|
|
|
233991
|
|
|
2
|
|
|
|
|
19
|
|
12
|
2
|
|
|
2
|
|
49537
|
use HTML::TreeBuilder; |
|
2
|
|
|
|
|
59140
|
|
|
2
|
|
|
|
|
23
|
|
13
|
2
|
|
|
2
|
|
624
|
use File::Spec::Functions qw(catfile); |
|
2
|
|
|
|
|
922
|
|
|
2
|
|
|
|
|
184
|
|
14
|
2
|
|
|
2
|
|
989
|
use Text::Markdown::Hoedown; |
|
2
|
|
|
|
|
2884
|
|
|
2
|
|
|
|
|
4198
|
|
15
|
|
|
|
|
|
|
|
16
|
|
|
|
|
|
|
plugin_keywords qw( md2html ); |
17
|
|
|
|
|
|
|
|
18
|
|
|
|
|
|
|
has options => (is => 'rw', default => sub { {} }); |
19
|
|
|
|
|
|
|
has cache => (is => 'ro', from_config => 'defaults.cache', default => sub { 1 } ); |
20
|
|
|
|
|
|
|
has layout => (is => 'ro', from_config => 'defaults.layout', default => sub { 'main.tt' } ); |
21
|
|
|
|
|
|
|
has template => (is => 'ro', from_config => 'defaults.template', default => sub { 'index.tt' } ); |
22
|
|
|
|
|
|
|
has file_root => (is => 'ro', from_config => 'defaults.file_root', default => sub { 'lib/data/markdown_files' } ); |
23
|
|
|
|
|
|
|
has route_root => (is => 'ro', from_config => 'defaults.route_root', default => sub { '' } ); |
24
|
|
|
|
|
|
|
has header_class => (is => 'ro', from_config => 'defaults.header_class', default => sub { '' } ); |
25
|
|
|
|
|
|
|
has generate_toc => (is => 'ro', from_config => 'defaults.generate_toc', default => sub { 0 } ); |
26
|
|
|
|
|
|
|
has exclude_files => (is => 'ro', from_config => 'defaults.exclude_files', default => sub { [] } ); |
27
|
|
|
|
|
|
|
has include_files => (is => 'ro', from_config => 'defaults.include_files', default => sub { [] } ); |
28
|
|
|
|
|
|
|
has linkable_headers => (is => 'ro', from_config => 'defaults.linkable_headers', default => sub { 0 } ); |
29
|
|
|
|
|
|
|
has markdown_extensions => (is => 'ro', from_config => 'defaults.markdown_extensions', default => sub { [] } ); |
30
|
|
|
|
|
|
|
|
31
|
|
|
|
|
|
|
# Builds the routes from config file |
32
|
|
|
|
|
|
|
sub BUILD { |
33
|
1
|
|
|
1
|
0
|
113
|
my $s = shift; |
34
|
1
|
|
|
|
|
9
|
my $app = $s->app; |
35
|
1
|
|
|
|
|
24
|
my $config = $s->config; |
36
|
|
|
|
|
|
|
|
37
|
|
|
|
|
|
|
# add routes from config file |
38
|
1
|
|
|
|
|
57
|
foreach my $route (@{$config->{routes}}) { |
|
1
|
|
|
|
|
4
|
|
39
|
|
|
|
|
|
|
|
40
|
|
|
|
|
|
|
# validate arguments supplied from config file |
41
|
1
|
50
|
|
|
|
7
|
if ((ref $route) ne 'HASH') { |
42
|
0
|
|
|
|
|
0
|
die 'Config file misconfigured. Check syntax or consult documentation.'; |
43
|
|
|
|
|
|
|
} |
44
|
|
|
|
|
|
|
|
45
|
1
|
|
|
|
|
4
|
$s->_set_options($route); |
46
|
|
|
|
|
|
|
|
47
|
|
|
|
|
|
|
# Do the route addin' |
48
|
|
|
|
|
|
|
|
49
|
1
|
|
|
|
|
2
|
my %options = %{$s->options}; |
|
1
|
|
|
|
|
9
|
|
50
|
|
|
|
|
|
|
$s->app->add_route( |
51
|
|
|
|
|
|
|
method => 'get', |
52
|
|
|
|
|
|
|
regexp => '/' . $options{route_root} . $options{path}, |
53
|
|
|
|
|
|
|
code => sub { |
54
|
1
|
|
|
1
|
|
197346
|
my $app = shift; |
55
|
1
|
|
|
|
|
10
|
my ($html, $toc) = $s->md2html($options{resource}, \%options); |
56
|
|
|
|
|
|
|
$app->template($options{template}, |
57
|
|
|
|
|
|
|
{ html => $html, toc => $toc }, |
58
|
1
|
|
|
|
|
28
|
{ layout => $options{layout} }); |
59
|
|
|
|
|
|
|
}, |
60
|
1
|
|
|
|
|
14
|
); |
61
|
|
|
|
|
|
|
} |
62
|
|
|
|
|
|
|
} |
63
|
|
|
|
|
|
|
|
64
|
|
|
|
|
|
|
sub _set_options { |
65
|
6
|
|
|
6
|
|
15
|
my $s = shift; |
66
|
6
|
|
|
|
|
19
|
my $options = shift; |
67
|
|
|
|
|
|
|
|
68
|
6
|
|
|
|
|
33
|
my @settings = qw( |
69
|
|
|
|
|
|
|
cache layout template file_root route_root header_class generate_toc |
70
|
|
|
|
|
|
|
exclude_files include_files linkable_headers markdown_extensions ); |
71
|
|
|
|
|
|
|
|
72
|
6
|
|
|
|
|
33
|
my ($path) = keys %$options; |
73
|
6
|
|
|
|
|
136
|
my $defaults = $s->config->{defaults}; |
74
|
6
|
100
|
|
|
|
104
|
my $local_options = (ref $options->{$path}) ? $options->{$path} : $options; |
75
|
6
|
|
|
|
|
38
|
my %options = (%$defaults, %$local_options); |
76
|
|
|
|
|
|
|
|
77
|
6
|
|
|
|
|
24
|
foreach my $setting (@settings) { |
78
|
66
|
|
100
|
|
|
1836
|
$options{$setting} = $options->{$setting} //= $s->$setting; |
79
|
|
|
|
|
|
|
} |
80
|
|
|
|
|
|
|
|
81
|
6
|
|
|
|
|
88
|
$options{set} = 1; |
82
|
6
|
|
|
|
|
16
|
$options{path} = $path; |
83
|
6
|
50
|
|
|
|
34
|
$options{route_root} .= '/' if $options{route_root}; |
84
|
6
|
100
|
|
|
|
28
|
$options{linkable_headers} = 1 if $options{generate_toc}; |
85
|
|
|
|
|
|
|
|
86
|
6
|
|
|
|
|
69
|
my $is_abs = File::Spec->file_name_is_absolute($options{resource}); |
87
|
6
|
50
|
|
|
|
21
|
if (!$is_abs) { |
88
|
6
|
|
|
|
|
70
|
$options{resource} = File::Spec->catfile($options{file_root}, $options{resource}); |
89
|
|
|
|
|
|
|
} |
90
|
|
|
|
|
|
|
|
91
|
6
|
|
|
|
|
50
|
$s->options(\%options); |
92
|
|
|
|
|
|
|
|
93
|
|
|
|
|
|
|
} |
94
|
|
|
|
|
|
|
|
95
|
|
|
|
|
|
|
# Keyword for generating HTML from a markdown resource (file or directory) |
96
|
|
|
|
|
|
|
# either through cache retrieval or parsing of the resource |
97
|
|
|
|
|
|
|
sub md2html { |
98
|
6
|
|
|
6
|
1
|
64422
|
my ($s, $resource, $options) = @_; |
99
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
# If keyword called directly, options won't be set yet |
101
|
6
|
100
|
|
|
|
26
|
if (!$options->{set}) { |
102
|
5
|
|
|
|
|
15
|
$options->{resource} = $resource; |
103
|
5
|
|
|
|
|
26
|
$s->_set_options($options); |
104
|
|
|
|
|
|
|
} else { |
105
|
1
|
|
|
|
|
13
|
$s->options($options); |
106
|
|
|
|
|
|
|
} |
107
|
|
|
|
|
|
|
|
108
|
6
|
50
|
|
|
|
161
|
if (!-e $s->options->{resource}) { |
109
|
|
|
|
|
|
|
my $return = 'This route is not properly configured. Resource: ' |
110
|
0
|
|
|
|
|
0
|
. $s->options->{resource} . ' does not exist on the server.'; |
111
|
0
|
|
|
|
|
0
|
$s->options({}); |
112
|
0
|
0
|
|
|
|
0
|
return wantarray ? ($return, '') : $return; |
113
|
|
|
|
|
|
|
} |
114
|
|
|
|
|
|
|
|
115
|
6
|
|
|
|
|
29
|
my $html = ''; |
116
|
6
|
|
|
|
|
25
|
my $toc = ''; |
117
|
|
|
|
|
|
|
|
118
|
6
|
|
|
|
|
17
|
my @files = (); |
119
|
6
|
100
|
|
|
|
105
|
if (-f $s->options->{resource}) { |
120
|
2
|
|
|
|
|
14
|
push @files, $s->options->{resource}; |
121
|
|
|
|
|
|
|
} else { |
122
|
4
|
|
|
|
|
28
|
my $dir = $s->options->{resource}; |
123
|
|
|
|
|
|
|
# gather the files according the options supplied |
124
|
4
|
50
|
|
|
|
9
|
if (@{$s->options->{include_files}}) { |
|
4
|
|
|
|
|
24
|
|
125
|
0
|
|
|
|
|
0
|
my @files = @{$s->options->{include_files}}; |
|
0
|
|
|
|
|
0
|
|
126
|
|
|
|
|
|
|
} else { |
127
|
4
|
50
|
|
|
|
157
|
opendir my $d, $dir or die "Cannot open directory: $!"; |
128
|
4
|
|
|
|
|
183
|
@files = grep { $_ !~ /^\./ } readdir $d; |
|
72
|
|
|
|
|
193
|
|
129
|
4
|
|
|
|
|
70
|
closedir $d; |
130
|
4
|
|
|
|
|
18
|
my @matching_files = (); |
131
|
4
|
|
|
|
|
8
|
foreach my $md_ext (@{$s->options->{markdown_extensions}}) { |
|
4
|
|
|
|
|
29
|
|
132
|
0
|
|
|
|
|
0
|
push @matching_files, grep { $_ =~ /\.$md_ext$/ } @files; |
|
0
|
|
|
|
|
0
|
|
133
|
|
|
|
|
|
|
} |
134
|
4
|
50
|
|
|
|
29
|
@files = @matching_files if @matching_files; |
135
|
|
|
|
|
|
|
} |
136
|
|
|
|
|
|
|
|
137
|
4
|
|
|
|
|
10
|
foreach my $excluded_file (@{$s->options->{exclude_files}}) { |
|
4
|
|
|
|
|
19
|
|
138
|
0
|
|
|
|
|
0
|
@files = grep { $_ ne $excluded_file } @files; |
|
0
|
|
|
|
|
0
|
|
139
|
|
|
|
|
|
|
} |
140
|
|
|
|
|
|
|
|
141
|
4
|
|
|
|
|
18
|
@files = map { File::Spec->catfile($dir, $_) } @files; |
|
64
|
|
|
|
|
390
|
|
142
|
|
|
|
|
|
|
} |
143
|
|
|
|
|
|
|
|
144
|
|
|
|
|
|
|
# concatenate html and toc into two strings |
145
|
6
|
|
|
|
|
42
|
foreach my $file (sort @files) { |
146
|
66
|
|
|
|
|
339
|
my ($file_html, $file_toc) = $s->_mdfile_2html($file); |
147
|
66
|
|
|
|
|
698
|
$html .= $file_html; |
148
|
66
|
|
100
|
|
|
507
|
$toc .= $file_toc || ''; |
149
|
|
|
|
|
|
|
} |
150
|
6
|
100
|
|
|
|
418
|
return wantarray ? ($html, $toc) : $html; |
151
|
|
|
|
|
|
|
} |
152
|
|
|
|
|
|
|
|
153
|
|
|
|
|
|
|
# Sends the markdown file to get parsed or retrieves html version from cache, |
154
|
|
|
|
|
|
|
# if available. Also generates the table of contents. |
155
|
|
|
|
|
|
|
sub _mdfile_2html { |
156
|
66
|
|
|
66
|
|
154
|
my $s = shift; |
157
|
66
|
|
|
|
|
161
|
my $file = shift; |
158
|
|
|
|
|
|
|
|
159
|
|
|
|
|
|
|
# chop off extension |
160
|
66
|
|
|
|
|
2862
|
my ($base) = fileparse($file, qr/\.[^.]*/); |
161
|
|
|
|
|
|
|
|
162
|
|
|
|
|
|
|
# generate the cache directory if it doesn't exist |
163
|
66
|
|
|
|
|
3190
|
my $cache_dir = File::Spec->catfile(dirname($s->options->{'file_root'}), 'md_file_cache'); |
164
|
66
|
100
|
|
|
|
1382
|
if (!-d $cache_dir) { |
165
|
1
|
50
|
|
|
|
233
|
make_path $cache_dir or die "Cannot make cache directory $!"; |
166
|
|
|
|
|
|
|
} |
167
|
|
|
|
|
|
|
|
168
|
|
|
|
|
|
|
# generate unique cache file name appended with values of two options |
169
|
66
|
|
|
|
|
2028
|
my $cache_file = dirname($file); |
170
|
66
|
|
|
|
|
614
|
my $sep = File::Spec->catfile('', ''); |
171
|
66
|
|
|
|
|
461
|
$cache_file =~ s/\Q$sep\E//g; |
172
|
|
|
|
|
|
|
$cache_file = File::Spec->catfile($cache_dir, |
173
|
|
|
|
|
|
|
$cache_file . $base . $s->options->{linkable_headers} . $s->options->{generate_toc} |
174
|
66
|
|
|
|
|
837
|
. $s->options->{header_class} =~ s/ //gr); |
175
|
|
|
|
|
|
|
|
176
|
|
|
|
|
|
|
# check for cache hit |
177
|
|
|
|
|
|
|
# TODO: Save options in separate file so they can be compared |
178
|
66
|
50
|
66
|
|
|
1991
|
if (-f $cache_file && $s->options->{cache}) { |
179
|
16
|
50
|
|
|
|
508
|
if (-M $cache_file eq -M $file) { |
180
|
16
|
50
|
33
|
|
|
143
|
print Dumper 'cache hit: '. $file if $ENV{DANCER_ENVIRONMENT} && $ENV{DANCER_ENVIRONMENT} eq 'testing'; |
181
|
16
|
|
|
|
|
1185
|
my $data = retrieve $cache_file; |
182
|
16
|
|
|
|
|
1637
|
return ($data->{html}, $data->{toc}); |
183
|
|
|
|
|
|
|
} |
184
|
|
|
|
|
|
|
} |
185
|
|
|
|
|
|
|
|
186
|
|
|
|
|
|
|
# no cache hit so we parse the file |
187
|
|
|
|
|
|
|
# slurp the file and parse it with Hoedown's markdown function |
188
|
50
|
|
|
|
|
232
|
my $markdown = ''; |
189
|
|
|
|
|
|
|
{ |
190
|
50
|
|
|
|
|
114
|
local $/; |
|
50
|
|
|
|
|
285
|
|
191
|
50
|
50
|
|
|
|
2326
|
open my $md, '<:encoding(UTF-8)', $file or die "Can't open $file: $!"; |
192
|
50
|
|
|
|
|
6619
|
$markdown = <$md>; |
193
|
50
|
|
|
|
|
4096
|
close $md; |
194
|
|
|
|
|
|
|
} |
195
|
50
|
|
|
|
|
390
|
my $out = markdown($markdown, extensions => HOEDOWN_EXT_FENCED_CODE, toc_nesting_lvl => 0); |
196
|
50
|
|
|
|
|
7987
|
my $tree = HTML::TreeBuilder->new_from_content($out); |
197
|
|
|
|
|
|
|
|
198
|
50
|
|
|
|
|
1752129
|
my @code_els = $tree->find_by_tag_name('code'); |
199
|
50
|
|
|
|
|
45143
|
foreach my $code_el (@code_els) { |
200
|
2711
|
100
|
100
|
|
|
49913
|
if (!$code_el->left && !$code_el->right) { |
201
|
345
|
|
|
|
|
5575
|
$code_el->attr('class' => 'single-line'); |
202
|
|
|
|
|
|
|
} |
203
|
|
|
|
|
|
|
} |
204
|
|
|
|
|
|
|
|
205
|
|
|
|
|
|
|
# See if we can cache and return the output without further processing |
206
|
|
|
|
|
|
|
# generate_toc makes linkable_headers true so we just need to test linkable_headers option |
207
|
50
|
100
|
100
|
|
|
1349
|
if (!$s->options->{linkable_headers} && !$s->options->{header_class}) { |
208
|
17
|
|
|
|
|
76
|
my $html = $tree->guts->as_HTML; |
209
|
17
|
50
|
|
|
|
255470
|
_cache_data($cache_file, $file, $html) if $s->options->{cache}; |
210
|
17
|
|
|
|
|
844
|
return ($html, ''); |
211
|
|
|
|
|
|
|
} |
212
|
|
|
|
|
|
|
|
213
|
|
|
|
|
|
|
# generate linkable headers along with toc if option for it is set |
214
|
33
|
|
|
|
|
344
|
my @elements = $tree->look_down(_tag => qr/^h\d$/); |
215
|
33
|
100
|
|
|
|
37242
|
my $toc = HTML::TreeBuilder->new() if $s->options->{generate_toc}; |
216
|
33
|
|
|
|
|
4244
|
my $hdr_ct = 0; |
217
|
33
|
|
|
|
|
90
|
foreach my $element (@elements) { |
218
|
246
|
|
|
|
|
3623
|
my $id = 'header_' . ${hdr_ct}; |
219
|
246
|
|
|
|
|
416
|
$hdr_ct++; |
220
|
246
|
|
|
|
|
913
|
$element->attr('id', $id . '_' . $base); |
221
|
246
|
100
|
|
|
|
3845
|
$element->attr('class' => $s->options->{header_class}) if $s->options->{header_class}; |
222
|
246
|
100
|
|
|
|
1917
|
if ($s->options->{generate_toc}) { |
223
|
117
|
|
|
|
|
250
|
my ($level) = $element->tag =~ /(\d)/; |
224
|
117
|
|
|
|
|
1319
|
my $toc_link = HTML::Element->new('a', href=> "#${id}_${base}", class => 'header_' . $level); |
225
|
117
|
|
|
|
|
4089
|
$toc_link->push_content($element->as_text); |
226
|
117
|
|
|
|
|
4835
|
$toc->push_content($toc_link); |
227
|
117
|
|
|
|
|
1790
|
$toc->push_content(HTML::Element->new('br')); |
228
|
|
|
|
|
|
|
} |
229
|
|
|
|
|
|
|
} |
230
|
|
|
|
|
|
|
|
231
|
|
|
|
|
|
|
# Generate the final HTML from trees and cache |
232
|
|
|
|
|
|
|
# "guts" method gets rid of and tags added by TreeBuilder |
233
|
33
|
100
|
|
|
|
656
|
my ($html, $toc_out) = ($tree->guts->as_HTML, $toc ? $toc->guts->as_HTML : ''); |
234
|
33
|
|
|
|
|
550646
|
_cache_data($cache_file, $file, $html, $toc_out); |
235
|
33
|
|
|
|
|
2087
|
return ($html, $toc_out); |
236
|
|
|
|
|
|
|
} |
237
|
|
|
|
|
|
|
|
238
|
|
|
|
|
|
|
sub _cache_data { |
239
|
50
|
|
|
50
|
|
215
|
my ($cache_file, $file, $content, $toc) = @_; |
240
|
50
|
|
100
|
|
|
228
|
$toc //= ''; |
241
|
|
|
|
|
|
|
|
242
|
50
|
|
|
|
|
433
|
store { html => $content, toc => $toc }, $cache_file; |
243
|
50
|
|
|
|
|
16714
|
my ($read, $write) = (stat($file))[8,9]; |
244
|
50
|
|
|
|
|
1083
|
utime($read, $write, $cache_file); |
245
|
|
|
|
|
|
|
} |
246
|
|
|
|
|
|
|
|
247
|
|
|
|
|
|
|
1; |
248
|
|
|
|
|
|
|
|
249
|
|
|
|
|
|
|
__END__ |