line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
package Dancer2::Plugin::MarkdownFilesToHTML ; |
2
|
|
|
|
|
|
|
$Dancer2::Plugin::MarkdownFilesToHTML::VERSION = '0.017'; |
3
|
|
|
|
|
|
|
# ABSTRACT: Easy conversion of markdown documents to HTML for display in your Dancer2 website |
4
|
2
|
|
|
2
|
|
1188394
|
use 5.010; use strict; use warnings; |
|
2
|
|
|
2
|
|
15
|
|
|
2
|
|
|
2
|
|
9
|
|
|
2
|
|
|
|
|
5
|
|
|
2
|
|
|
|
|
42
|
|
|
2
|
|
|
|
|
16
|
|
|
2
|
|
|
|
|
7
|
|
|
2
|
|
|
|
|
74
|
|
5
|
|
|
|
|
|
|
|
6
|
2
|
|
|
2
|
|
13
|
use Carp; |
|
2
|
|
|
|
|
3
|
|
|
2
|
|
|
|
|
146
|
|
7
|
2
|
|
|
2
|
|
549
|
use Encode qw( decode ); |
|
2
|
|
|
|
|
10305
|
|
|
2
|
|
|
|
|
113
|
|
8
|
2
|
|
|
2
|
|
594
|
use Storable; |
|
2
|
|
|
|
|
3187
|
|
|
2
|
|
|
|
|
164
|
|
9
|
2
|
|
|
2
|
|
22
|
use File::Path qw(make_path); |
|
2
|
|
|
|
|
7
|
|
|
2
|
|
|
|
|
132
|
|
10
|
2
|
|
|
2
|
|
604
|
use Data::Dumper 'Dumper'; |
|
2
|
|
|
|
|
6137
|
|
|
2
|
|
|
|
|
107
|
|
11
|
2
|
|
|
2
|
|
14
|
use File::Basename; |
|
2
|
|
|
|
|
4
|
|
|
2
|
|
|
|
|
165
|
|
12
|
2
|
|
|
2
|
|
1050
|
use Dancer2::Plugin; |
|
2
|
|
|
|
|
245011
|
|
|
2
|
|
|
|
|
20
|
|
13
|
2
|
|
|
2
|
|
47364
|
use HTML::TreeBuilder; |
|
2
|
|
|
|
|
63879
|
|
|
2
|
|
|
|
|
28
|
|
14
|
2
|
|
|
2
|
|
695
|
use File::Spec::Functions qw(catfile); |
|
2
|
|
|
|
|
878
|
|
|
2
|
|
|
|
|
173
|
|
15
|
2
|
|
|
2
|
|
1127
|
use Text::Markdown::Hoedown; |
|
2
|
|
|
|
|
2635
|
|
|
2
|
|
|
|
|
3825
|
|
16
|
|
|
|
|
|
|
|
17
|
|
|
|
|
|
|
plugin_keywords qw( md2html ); |
18
|
|
|
|
|
|
|
|
19
|
|
|
|
|
|
|
has options => (is => 'rw', default => sub { {} }); |
20
|
|
|
|
|
|
|
has cache => (is => 'ro', from_config => 'defaults.cache', default => sub { 1 } ); |
21
|
|
|
|
|
|
|
has prefix => (is => 'ro', from_config => 'defaults.prefix', default => sub { '' } ); |
22
|
|
|
|
|
|
|
has layout => (is => 'ro', from_config => 'defaults.layout', default => sub { 'main.tt' } ); |
23
|
|
|
|
|
|
|
has template => (is => 'ro', from_config => 'defaults.template', default => sub { 'index.tt' } ); |
24
|
|
|
|
|
|
|
has file_root => (is => 'ro', from_config => 'defaults.file_root', default => sub { 'lib/data/markdown_files' } ); |
25
|
|
|
|
|
|
|
has header_class => (is => 'ro', from_config => 'defaults.header_class', default => sub { '' } ); |
26
|
|
|
|
|
|
|
has generate_toc => (is => 'ro', from_config => 'defaults.generate_toc', default => sub { 0 } ); |
27
|
|
|
|
|
|
|
has exclude_files => (is => 'ro', from_config => 'defaults.exclude_files', default => sub { [] } ); |
28
|
|
|
|
|
|
|
has include_files => (is => 'ro', from_config => 'defaults.include_files', default => sub { [] } ); |
29
|
|
|
|
|
|
|
has linkable_headers => (is => 'ro', from_config => 'defaults.linkable_headers', default => sub { 0 } ); |
30
|
|
|
|
|
|
|
has markdown_extensions => (is => 'ro', from_config => 'defaults.markdown_extensions', default => sub { [] } ); |
31
|
|
|
|
|
|
|
|
32
|
|
|
|
|
|
|
# Builds the routes from the config file |
33
|
|
|
|
|
|
|
sub BUILD { |
34
|
1
|
|
|
1
|
0
|
95
|
my $s = shift; |
35
|
|
|
|
|
|
|
|
36
|
|
|
|
|
|
|
# add routes from config file |
37
|
1
|
|
|
|
|
2
|
foreach my $route (@{$s->config->{routes}}) { |
|
1
|
|
|
|
|
21
|
|
38
|
|
|
|
|
|
|
|
39
|
|
|
|
|
|
|
# validate arguments supplied from config file |
40
|
3
|
50
|
|
|
|
13308
|
if ((ref $route) ne 'HASH') { |
41
|
0
|
|
|
|
|
0
|
die 'Config file misconfigured. Check syntax or consult documentation.'; |
42
|
|
|
|
|
|
|
} |
43
|
|
|
|
|
|
|
|
44
|
3
|
|
|
|
|
15
|
$s->_set_options($route); |
45
|
3
|
|
|
|
|
5
|
my %options = %{$s->options}; |
|
3
|
|
|
|
|
30
|
|
46
|
|
|
|
|
|
|
$s->app->add_route( |
47
|
|
|
|
|
|
|
method => 'get', |
48
|
|
|
|
|
|
|
regexp => '/' . $options{prefix} . $options{path}, |
49
|
|
|
|
|
|
|
code => sub { |
50
|
3
|
|
|
3
|
|
396762
|
my ($html, $toc) = $s->md2html($options{resource}, \%options); |
51
|
|
|
|
|
|
|
$s->app->template($options{template}, |
52
|
|
|
|
|
|
|
{ html => $html, toc => $toc }, |
53
|
3
|
|
|
|
|
58
|
{ layout => $options{layout} }); |
54
|
|
|
|
|
|
|
}, |
55
|
3
|
|
|
|
|
39
|
); |
56
|
|
|
|
|
|
|
} |
57
|
|
|
|
|
|
|
} |
58
|
|
|
|
|
|
|
|
59
|
|
|
|
|
|
|
sub _set_options { |
60
|
8
|
|
|
8
|
|
22
|
my ($s, $options) = @_; |
61
|
|
|
|
|
|
|
|
62
|
8
|
|
|
|
|
47
|
my @settings = qw( cache layout template file_root prefix header_class |
63
|
|
|
|
|
|
|
generate_toc exclude_files include_files linkable_headers markdown_extensions ); |
64
|
|
|
|
|
|
|
|
65
|
8
|
|
|
|
|
30
|
my ($path) = keys %$options; |
66
|
8
|
|
|
|
|
158
|
my $defaults = $s->config->{defaults}; |
67
|
8
|
100
|
|
|
|
100
|
my $local_options = (ref $options->{$path}) ? $options->{$path} : $options; |
68
|
8
|
|
|
|
|
59
|
my %options = (%$defaults, %$local_options); |
69
|
|
|
|
|
|
|
|
70
|
8
|
|
|
|
|
26
|
foreach my $setting (@settings) { |
71
|
88
|
|
100
|
|
|
1483
|
$options{$setting} = $options{$setting} // $s->$setting; |
72
|
|
|
|
|
|
|
} |
73
|
|
|
|
|
|
|
|
74
|
8
|
|
|
|
|
72
|
$options{set} = 1; |
75
|
8
|
|
|
|
|
25
|
$options{path} = $path; |
76
|
8
|
100
|
|
|
|
26
|
$options{prefix} .= '/' if $options{prefix}; |
77
|
8
|
100
|
|
|
|
22
|
$options{linkable_headers} = 1 if $options{generate_toc}; |
78
|
|
|
|
|
|
|
|
79
|
8
|
50
|
|
|
|
87
|
if (!File::Spec->file_name_is_absolute($options{resource})) { |
80
|
8
|
|
|
|
|
110
|
$options{resource} = File::Spec->catfile($options{file_root}, $options{resource}); |
81
|
|
|
|
|
|
|
} |
82
|
8
|
|
|
|
|
67
|
$s->options(\%options); |
83
|
|
|
|
|
|
|
} |
84
|
|
|
|
|
|
|
|
85
|
|
|
|
|
|
|
# Keyword for generating HTML |
86
|
|
|
|
|
|
|
sub md2html { |
87
|
8
|
|
|
8
|
1
|
73887
|
my ($s, $resource, $options) = @_; |
88
|
|
|
|
|
|
|
|
89
|
|
|
|
|
|
|
# If keyword called directly, options won't be set yet |
90
|
8
|
100
|
|
|
|
35
|
if (!$options->{set}) { |
91
|
5
|
|
|
|
|
16
|
$options->{resource} = $resource; |
92
|
5
|
|
|
|
|
24
|
$s->_set_options($options); |
93
|
|
|
|
|
|
|
} else { |
94
|
3
|
|
|
|
|
24
|
$s->options($options); |
95
|
|
|
|
|
|
|
} |
96
|
|
|
|
|
|
|
|
97
|
|
|
|
|
|
|
#TODO: return a 202 status code |
98
|
8
|
100
|
|
|
|
311
|
if (!-e $s->options->{resource}) { |
99
|
|
|
|
|
|
|
my $html = 'This route is not properly configured. Resource: ' |
100
|
1
|
|
|
|
|
11
|
. $s->options->{resource} . ' does not exist on the server.'; |
101
|
1
|
|
|
|
|
6
|
return ($html, undef); |
102
|
|
|
|
|
|
|
} |
103
|
|
|
|
|
|
|
|
104
|
7
|
|
|
|
|
38
|
my @files = $s->_gather_files; |
105
|
7
|
|
|
|
|
24
|
my ($html, $toc) = ''; |
106
|
7
|
|
|
|
|
42
|
foreach my $file (sort @files) { |
107
|
67
|
|
|
|
|
293
|
my ($file_html, $file_toc) = $s->_mdfile_2html($file); |
108
|
67
|
|
|
|
|
842
|
$html .= $file_html; |
109
|
67
|
|
|
|
|
392
|
$toc .= $file_toc; |
110
|
|
|
|
|
|
|
} |
111
|
7
|
100
|
|
|
|
399
|
return wantarray ? ($html, $toc) : $html; |
112
|
|
|
|
|
|
|
} |
113
|
|
|
|
|
|
|
|
114
|
|
|
|
|
|
|
sub _gather_files { |
115
|
7
|
|
|
7
|
|
16
|
my $s = shift; |
116
|
|
|
|
|
|
|
|
117
|
7
|
|
|
|
|
15
|
my @files; |
118
|
7
|
100
|
|
|
|
100
|
if (-f $s->options->{resource}) { |
119
|
3
|
|
|
|
|
19
|
push @files, $s->options->{resource}; |
120
|
|
|
|
|
|
|
} else { |
121
|
4
|
|
|
|
|
59
|
my $dir = $s->options->{resource}; |
122
|
|
|
|
|
|
|
# gather the files according the options supplied |
123
|
4
|
50
|
|
|
|
7
|
if (@{$s->options->{include_files}}) { |
|
4
|
|
|
|
|
20
|
|
124
|
0
|
|
|
|
|
0
|
my @files = @{$s->options->{include_files}}; |
|
0
|
|
|
|
|
0
|
|
125
|
|
|
|
|
|
|
} else { |
126
|
4
|
50
|
|
|
|
165
|
opendir my $d, $dir or die "Cannot open directory: $!"; |
127
|
4
|
|
|
|
|
164
|
@files = grep { $_ !~ /^\./ } readdir $d; |
|
72
|
|
|
|
|
182
|
|
128
|
4
|
|
|
|
|
66
|
closedir $d; |
129
|
4
|
|
|
|
|
15
|
my @matching_files = (); |
130
|
4
|
|
|
|
|
9
|
foreach my $md_ext (@{$s->options->{markdown_extensions}}) { |
|
4
|
|
|
|
|
21
|
|
131
|
0
|
|
|
|
|
0
|
push @matching_files, grep { $_ =~ /\.$md_ext$/ } @files; |
|
0
|
|
|
|
|
0
|
|
132
|
|
|
|
|
|
|
} |
133
|
4
|
50
|
|
|
|
26
|
@files = @matching_files if @matching_files; |
134
|
|
|
|
|
|
|
} |
135
|
|
|
|
|
|
|
|
136
|
4
|
|
|
|
|
9
|
foreach my $excluded_file (@{$s->options->{exclude_files}}) { |
|
4
|
|
|
|
|
16
|
|
137
|
0
|
|
|
|
|
0
|
@files = grep { $_ ne $excluded_file } @files; |
|
0
|
|
|
|
|
0
|
|
138
|
|
|
|
|
|
|
} |
139
|
|
|
|
|
|
|
|
140
|
4
|
|
|
|
|
11
|
@files = map { File::Spec->catfile($dir, $_) } @files; |
|
64
|
|
|
|
|
331
|
|
141
|
|
|
|
|
|
|
} |
142
|
7
|
|
|
|
|
35
|
return @files; |
143
|
|
|
|
|
|
|
} |
144
|
|
|
|
|
|
|
|
145
|
|
|
|
|
|
|
# Sends the markdown file to get parsed or retrieves html version from cache, |
146
|
|
|
|
|
|
|
# if available. Also generates the table of contents. |
147
|
|
|
|
|
|
|
sub _mdfile_2html { |
148
|
67
|
|
|
67
|
|
150
|
my $s = shift; |
149
|
67
|
|
|
|
|
173
|
my $file = shift; |
150
|
|
|
|
|
|
|
|
151
|
|
|
|
|
|
|
# chop off extension |
152
|
67
|
|
|
|
|
4446
|
my ($base) = fileparse($file, qr/\.[^.]*/); |
153
|
|
|
|
|
|
|
|
154
|
|
|
|
|
|
|
# generate the cache directory if it doesn't exist |
155
|
67
|
|
|
|
|
6152
|
my $cache_dir = File::Spec->catfile(dirname($s->options->{'file_root'}), 'md_file_cache'); |
156
|
67
|
100
|
|
|
|
1406
|
if (!-d $cache_dir) { |
157
|
1
|
50
|
|
|
|
640
|
make_path $cache_dir or die "Cannot make cache directory $!"; |
158
|
|
|
|
|
|
|
} |
159
|
|
|
|
|
|
|
|
160
|
|
|
|
|
|
|
# generate unique cache file name appended with options |
161
|
67
|
|
|
|
|
2516
|
my $cache_file = dirname($file); |
162
|
67
|
|
|
|
|
656
|
my $sep = File::Spec->catfile('', ''); |
163
|
67
|
|
|
|
|
4753
|
$cache_file =~ s/\Q$sep\E//g; |
164
|
67
|
|
|
|
|
627
|
my $header_classes = $s->options->{header_class}; |
165
|
67
|
|
|
|
|
169
|
$header_classes =~ s/ //g; |
166
|
|
|
|
|
|
|
$cache_file = File::Spec->catfile($cache_dir, |
167
|
|
|
|
|
|
|
$cache_file |
168
|
|
|
|
|
|
|
. $base |
169
|
|
|
|
|
|
|
. $s->options->{linkable_headers} |
170
|
|
|
|
|
|
|
. $s->options->{generate_toc} |
171
|
67
|
|
|
|
|
811
|
. $header_classes); |
172
|
|
|
|
|
|
|
|
173
|
|
|
|
|
|
|
# check for cache hit |
174
|
|
|
|
|
|
|
# TODO: Save options in separate file so they can be compared |
175
|
67
|
50
|
66
|
|
|
6320
|
if (-f $cache_file && $s->options->{cache}) { |
176
|
17
|
50
|
|
|
|
391
|
if (-M $cache_file eq -M $file) { |
177
|
17
|
50
|
33
|
|
|
133
|
print Dumper 'cache hit: '. $file if $ENV{DANCER_ENVIRONMENT} && $ENV{DANCER_ENVIRONMENT} eq 'testing'; |
178
|
17
|
|
|
|
|
1065
|
my $data = retrieve $cache_file; |
179
|
17
|
|
|
|
|
1384
|
return ($data->{html}, $data->{toc}); |
180
|
|
|
|
|
|
|
} |
181
|
|
|
|
|
|
|
} |
182
|
|
|
|
|
|
|
|
183
|
|
|
|
|
|
|
# no cache hit so we parse the file |
184
|
|
|
|
|
|
|
# slurp the file and parse it with Hoedown's markdown function |
185
|
50
|
|
|
|
|
175
|
my $markdown = ''; |
186
|
|
|
|
|
|
|
{ |
187
|
50
|
|
|
|
|
166
|
local $/; |
|
50
|
|
|
|
|
413
|
|
188
|
50
|
50
|
|
|
|
2566
|
open my $md, '<:encoding(UTF-8)', $file or die "Can't open $file: $!"; |
189
|
50
|
|
|
|
|
7382
|
$markdown = <$md>; |
190
|
50
|
|
|
|
|
8694
|
close $md; |
191
|
|
|
|
|
|
|
} |
192
|
50
|
|
|
|
|
361
|
my $out = markdown($markdown, extensions => HOEDOWN_EXT_FENCED_CODE, toc_nesting_lvl => 0); |
193
|
50
|
|
|
|
|
8142
|
my $tree = HTML::TreeBuilder->new_from_content($out); |
194
|
|
|
|
|
|
|
|
195
|
50
|
|
|
|
|
1707173
|
my @code_els = $tree->find_by_tag_name('code'); |
196
|
50
|
|
|
|
|
38380
|
foreach my $code_el (@code_els) { |
197
|
2711
|
100
|
100
|
|
|
43716
|
if (!$code_el->left && !$code_el->right) { |
198
|
345
|
|
|
|
|
4769
|
$code_el->attr('class' => 'single-line'); |
199
|
|
|
|
|
|
|
} |
200
|
|
|
|
|
|
|
} |
201
|
|
|
|
|
|
|
|
202
|
|
|
|
|
|
|
# See if we can cache and return the output without further processing |
203
|
50
|
100
|
100
|
|
|
1333
|
if (!$s->options->{linkable_headers} && !$s->options->{header_class}) { |
204
|
17
|
|
|
|
|
100
|
my $html = $tree->guts->as_HTML; |
205
|
17
|
50
|
|
|
|
285723
|
_cache_data($cache_file, $file, $html) if $s->options->{cache}; |
206
|
17
|
|
|
|
|
1994
|
return ($html, ''); |
207
|
|
|
|
|
|
|
} |
208
|
|
|
|
|
|
|
|
209
|
|
|
|
|
|
|
# add linkable_headers, toc, and header_class per options |
210
|
33
|
|
|
|
|
438
|
my @elements = $tree->look_down(_tag => qr/^h\d$/); |
211
|
33
|
100
|
|
|
|
30032
|
my $toc = HTML::TreeBuilder->new() if $s->options->{generate_toc}; |
212
|
33
|
|
|
|
|
4193
|
my $hdr_ct = 0; |
213
|
33
|
|
|
|
|
88
|
foreach my $element (@elements) { |
214
|
246
|
|
|
|
|
2979
|
my $id = 'header_' . ${hdr_ct}; |
215
|
246
|
|
|
|
|
369
|
$hdr_ct++; |
216
|
246
|
|
|
|
|
762
|
$element->attr('id', $id . '_' . $base); |
217
|
246
|
100
|
|
|
|
3065
|
$element->attr('class' => $s->options->{header_class}) if $s->options->{header_class}; |
218
|
246
|
100
|
|
|
|
1523
|
if ($s->options->{generate_toc}) { |
219
|
117
|
|
|
|
|
219
|
my ($level) = $element->tag =~ /(\d)/; |
220
|
117
|
|
|
|
|
1053
|
my $toc_link = HTML::Element->new('a', href=> "#${id}_${base}", class => 'header_' . $level); |
221
|
117
|
|
|
|
|
3282
|
$toc_link->push_content($element->as_text); |
222
|
117
|
|
|
|
|
3948
|
$toc->push_content($toc_link); |
223
|
117
|
|
|
|
|
1462
|
$toc->push_content(HTML::Element->new('br')); |
224
|
|
|
|
|
|
|
} |
225
|
|
|
|
|
|
|
} |
226
|
|
|
|
|
|
|
|
227
|
|
|
|
|
|
|
# Generate the final HTML from trees and cache |
228
|
|
|
|
|
|
|
# "guts" method gets rid of and tags added by TreeBuilder |
229
|
33
|
100
|
|
|
|
564
|
my ($html, $toc_out) = ($tree->guts->as_HTML, $toc ? $toc->guts->as_HTML : ''); |
230
|
33
|
|
|
|
|
454374
|
_cache_data($cache_file, $file, $html, $toc_out); |
231
|
33
|
|
|
|
|
2404
|
return ($html, $toc_out); |
232
|
|
|
|
|
|
|
} |
233
|
|
|
|
|
|
|
|
234
|
|
|
|
|
|
|
sub _cache_data { |
235
|
50
|
|
|
50
|
|
225
|
my ($cache_file, $file, $content, $toc) = @_; |
236
|
50
|
|
100
|
|
|
253
|
$toc //= ''; |
237
|
|
|
|
|
|
|
|
238
|
50
|
|
|
|
|
508
|
store { html => $content, toc => $toc }, $cache_file; |
239
|
50
|
|
|
|
|
18986
|
my ($read, $write) = (stat($file))[8,9]; |
240
|
50
|
|
|
|
|
1095
|
utime($read, $write, $cache_file); |
241
|
|
|
|
|
|
|
} |
242
|
|
|
|
|
|
|
|
243
|
|
|
|
|
|
|
1; |
244
|
|
|
|
|
|
|
|
245
|
|
|
|
|
|
|
__END__ |