1package DocSet::RunTime;
2
3# META: this class acts as Singleton, consider to actually use
4# Class::Singleton
5
6use strict;
7use warnings;
8
9use File::Spec::Functions qw(catdir catfile splitdir);
10use File::Find;
11
12use DocSet::Util;
13use Carp;
14
15use vars qw(@ISA @EXPORT %opts);
16@ISA    = qw(Exporter);
17@EXPORT = qw(get_opts find_src_doc set_render_obj get_render_obj unset_render_obj);
18
19my %registers = ();
20
21my @search_paths = ();
22my %src_docs = ();
23my %exts     = ();
24my $render_obj;
25
26sub registers_reset {
27    %registers = ();
28}
29
30sub register {
31    my ($register, $key, $val) = @_;
32    push @{$registers{$register}{$key}}, $val;
33    return @{ $registers{$register}{$key} || []};
34}
35
36
37sub set_opt {
38    my (%args) = ();
39    if (@_ == 1) {
40        my $arg = shift;
41        my $ref = ref $arg;
42        if ($ref) {
43            %args = $ref eq 'HASH' ? %$arg : @$arg;
44        } else {
45            die "must be a ref to or an array/hash";
46        }
47    } else {
48        %args = @_;
49    }
50    @opts{keys %args} = values %args;
51}
52
53sub get_opts {
54    my $opt = shift;
55    exists $opts{$opt} ? $opts{$opt} : '';
56}
57
58# check whether we have a Storable avalable
59use constant HAS_STORABLE => eval { require Storable; };
60sub has_storable_module {
61    return HAS_STORABLE;
62}
63
64# check for existence of html2ps and ps2pdf
65
66my $html2ps_exec = which('html2ps');
67sub can_create_ps {
68    # ps2html is bundled, so we can always create PS
69    return $html2ps_exec if $html2ps_exec;
70
71    print 'It seems that you do not have html2ps installed! You have',
72        'to install it if you want to generate the PDF file';
73    return 0;
74
75    # if you unbundle it make sure you write here a code similar to
76    # can_create_pdf()
77}
78
79my $ps2pdf_exec = which('ps2pdf');
80sub can_create_pdf {
81    # check whether ps2pdf exists
82    return $ps2pdf_exec if $ps2pdf_exec;
83
84    print 'It seems that you do not have ps2pdf installed! You have',
85        'to install it if you want to generate the PDF file';
86    return 0;
87}
88
89sub scan_src_docs {
90    my ($base, $ra_search_paths, $ra_search_exts) = @_;
91
92    @search_paths = @{$ra_search_paths || []};
93
94    # .cfg is for matching config.cfg to become index.html
95    %exts = map {$_ => 1} @{$ra_search_exts || []}, 'cfg';
96
97    my @ext_accept_pattern = map {quotemeta($_)."\$"} keys %exts;
98    my $rsub_keep_ext =
99        build_matchmany_sub(\@ext_accept_pattern);
100
101    my %seen;
102    for my $rel_path (@search_paths) {
103        my $full_base_path = catdir $base, $rel_path;
104        die "'search_paths' attr: $full_base_path is not a dir"
105            unless -d $full_base_path;
106
107        my @seen_pattern = map {"^".quotemeta($_)} keys %seen;
108        my $rsub_skip_seen = build_matchmany_sub(\@seen_pattern);
109
110        my $rel_uri = path2uri($rel_path);
111        $src_docs{$rel_uri} = {
112                                            # autogenerated index.html
113            map { s/config\.cfg$/index.html/; ($_ => 1) }
114                                            # full path => relative uri
115            map path2uri( DocSet::Util::abs2rel($_, $full_base_path) ),
116            grep $rsub_keep_ext->($_),      # get files with wanted exts
117            grep !$rsub_skip_seen->($_),    # skip seen base dirs
118            @{ expand_dir($full_base_path) }
119        };
120
121        note "Scanning for src files: $full_base_path";
122        $seen{$full_base_path}++;
123    }
124
125#   dumper \%src_docs;
126}
127
128# this function returns a URI, so its separators are always /
129sub find_src_doc {
130    my ($resource_rel_path) = @_;
131
132    # push the html extension, because of autogenerated index.html,
133    # which should be found automatically
134    $exts{html} = 1 unless exists $exts{html};
135
136    for my $path (keys %src_docs) {
137        if (my $found_path = match_in_doc_src_subset($path,
138                                                     $resource_rel_path)) {
139            return path2uri($found_path);
140        }
141    }
142
143    # if we didn't find anything so far, it's possible that the path was
144    # specified with a longer prefix, that was needed (the above
145    # searches only the end leaves), so try locate the segments of the
146    # search path and search within maching sub-sets
147    for my $path (@search_paths) {
148        if ($resource_rel_path =~ m|^$path/(.*)|) {
149            if (my $found_path = match_in_doc_src_subset($path, $1)) {
150                return path2uri($found_path);
151            }
152        }
153    }
154
155#dumper  $src_docs{"docs/1.0"};
156    return;
157}
158
159# accepts the base_path (from the @search_paths) and the rel_path as
160# args, then it tries to find the match by applying known extensions.
161#
162# if matched, returns the whole path relative to the root, otherwise
163# returns undef
164sub match_in_doc_src_subset {
165    my ($base_path, $rel_path) = @_;
166    for my $ext (keys %exts) {
167#print qq{Try:  $base_path :: $rel_path.$ext\n};
168        if (exists $src_docs{$base_path}{"$rel_path.$ext"}) {
169#print qq{Found $base_path/$rel_path.$ext\n};
170            return catdir $base_path, "$rel_path.$ext";
171        }
172    }
173    return;
174}
175
176# set render object: sort of Singleton, it'll complain aloud if the
177# object is set over the existing object, without first unsetting it
178sub set_render_obj {
179    Carp::croak("usage: set_render_obj(\$obj) ") unless @_;
180    Carp::croak("unset render_obj before setting a new one") if $render_obj;
181    Carp::croak("undefined render_obj passed") unless defined $_[0];
182    $render_obj = shift;
183}
184
185sub get_render_obj {
186    Carp::croak("render_obj is not available") unless $render_obj;
187
188    return $render_obj;
189}
190
191sub unset_render_obj {
192    Carp::croak("render_obj is not set") unless $render_obj;
193
194    undef $render_obj;
195}
196
197
1981;
199__END__
200
201=head1 NAME
202
203C<DocSet::RunTime> - RunTime Configuration
204
205=head1 SYNOPSIS
206
207  use DocSet::RunTime;
208
209  # run time options
210  DocSet::RunTime::set_opt(\%args);
211  if (get_opts('verbose') {
212      print "verbose mode";
213  }
214
215  # hosting system capabilities testing
216  DocSet::RunTime::has_storable_module();
217  DocSet::RunTime::can_create_ps();
218  DocSet::RunTime::can_create_pdf();
219
220  # source documents lookup
221  DocSet::RunTime::scan_src_docs($base_path, \@search_paths, \@search_exts);
222  my $full_src_path = find_src_doc($resource_rel_path);
223
224  # rendering object singleton
225  set_render_obj($obj);
226  unset_render_obj();
227  $obj = get_render_obj();
228
229=head1 DESCRIPTION
230
231This module is a part of the docset application, and it stores the run
232time arguments, caches results of expensive calls and provide
233Singleton-like service to the whole system.
234
235=head1 FUNCTIONS
236
237META: To be completed, see SYNOPSIS
238
239=head2 Run Time Options
240
241Only get_opts() method is exported by default.
242
243=over
244
245=item * registers_reset()
246
247This function resets various run-time registers, used for validations.
248
249If the runtime is run more than once remember to always run first this
250function and even better always run it before using the runtime. e.g.:
251
252  DocSet::RunTime::registers_reset();
253  my $docset = DocSet::DocSet::HTML->new($config_file);
254  $docset->set_dir(abs_root => ".");
255  $docset->scan;
256  $docset->render;
257
258=item * register
259
260  my @entries = register($register_name, $key, $val);
261
262Push into the register for a given key the supplied value.
263
264Return an array of the given register's key.
265
266For example used to track duplicated docset ids with:
267
268    my @entries = DocSet::RunTime::register('unique_docset_id', $id,
269                                           $self->{config_file});
270    die if @entries > 1;
271
272because if the register returns two value for the same key, someone
273has already registered that key before. The values in C<@entries>
274include the config files in this example.
275
276=item * set_opt(\%args)
277
278
279=item * get_opts()
280
281
282=back
283
284=head2 Hosting System Capabilities Testing
285
286These methods test the capability of the system and are a part of the
287runtime system to perform the checking only once.
288
289=over
290
291=item * has_storable_module
292
293
294=item * can_create_ps
295
296
297=item * can_create_pdf
298
299=back
300
301=head2 Source Documents Lookup
302
303A system for mapping L<> escapes to the located of the rendered
304files. This system scans once the C<@search_paths> for files with
305C<@search_exts> starting from C<$base_path> using scan_src_docs(). The
306C<@search_paths> and C<@search_exts> are configured in the
307I<config.cfg> file. For example:
308
309    dir => {
310             # search path for pods, etc. must put more specific paths first!
311             search_paths => [qw(
312                 foo/bar
313                 foo
314                 .
315             )],
316             # what extensions to search for
317             search_exts => [qw(pod pm html)],
318 	    },
319
320So for example if the base path is I<~/myproject/src>, the files with
321extensions I<.pod>, I<.pm> and I<.html> will be searched in
322I<~/myproject/src/foo/bar>, I<~/myproject/src/foo> and
323I<~/myproject/src>.
324
325Notice that you must specify more specific paths first, since for
326optimization the seen paths are skipped. Therefore in our example the
327more explicit path I<foo/bar> was listed before the more general
328I<foo>.
329
330When the POD parser finds a L<> sequence it indentifies the resource
331part and passes it to the find_src_doc() which looks up for this file
332in the cache and returns its original (src) location, which can be
333then easily converted to the final location and optionally adjusting
334the extension, e.g. when the POD file is converted to HTML.
335
336As a special extension this function automatically assumes that
337C<index.html> will be generated in each directory containing items of
338an interest. Therefore in find_src_doc() it'll automatically find
339things like: L<the guide|guide::index>, even though there was no
340source I<index.pod> or I<index.html> in first place.
341
342Only the find_src_doc() function is exported by default.
343
344=over
345
346=item * scan_src_docs($base_path, \@search_paths, \@search_exts);
347
348=item * find_src_doc($resource_rel_path);
349
350returns C<undef> if nothing was found. See the description above.
351
352=back
353
354
355=head2 Rendering Object Singleton
356
357Since the rendering process may happen by a third party system, into
358which we provide hooks or overload some of its methods, it's quite
359possible that we won't be able to access the current document (or
360better rendering) object. One solution would be to have a global
361package variable, but that's very error-prone. Therefore the used
362solution is to provide a hook into a RunTime environment setting the
363current rendering object when the rendering of a single page starts
364via C<set_render_obj($obj)> and unsetting it when it's finished via
365unset_render_obj(). Between these two moments the current rendering
366object can be retrieved with get_render_obj() method.
367
368Notice that this is all possible in the program which is not threaded,
369or/and only one rendering process exists at any given time from its
370start to its end.
371
372All three methods are exported by default.
373
374=over
375
376=item * set_render_obj($obj)
377
378Sets the current rendering object.
379
380You cannot set a new rendering object before the previous one is
381unset. This is in order to make sure that one document won't use by
382mistake a rendering object of another document. So when the rendering
383is done remember to call the unset_render_obj() function.
384
385=item * unset_render_obj()
386
387Unsets the currently set rendering object.
388
389=item * get_render_obj()
390
391Retrieves the currently set rendering object or complains aloud if it
392cannot find one.
393
394=back
395
396=head1 AUTHORS
397
398Stas Bekman E<lt>stas (at) stason.orgE<gt>
399
400=cut
401