package DocSet::RunTime; # META: this class acts as Singleton, consider to actually use # Class::Singleton use strict; use warnings; use File::Spec::Functions qw(catdir catfile splitdir); use File::Find; use DocSet::Util; use Carp; use vars qw(@ISA @EXPORT %opts); @ISA = qw(Exporter); @EXPORT = qw(get_opts find_src_doc set_render_obj get_render_obj unset_render_obj); my %registers = (); my @search_paths = (); my %src_docs = (); my %exts = (); my $render_obj; sub registers_reset { %registers = (); } sub register { my ($register, $key, $val) = @_; push @{$registers{$register}{$key}}, $val; return @{ $registers{$register}{$key} || []}; } sub set_opt { my (%args) = (); if (@_ == 1) { my $arg = shift; my $ref = ref $arg; if ($ref) { %args = $ref eq 'HASH' ? %$arg : @$arg; } else { die "must be a ref to or an array/hash"; } } else { %args = @_; } @opts{keys %args} = values %args; } sub get_opts { my $opt = shift; exists $opts{$opt} ? $opts{$opt} : ''; } # check whether we have a Storable avalable use constant HAS_STORABLE => eval { require Storable; }; sub has_storable_module { return HAS_STORABLE; } # check for existence of html2ps and ps2pdf my $html2ps_exec = which('html2ps'); sub can_create_ps { # ps2html is bundled, so we can always create PS return $html2ps_exec if $html2ps_exec; print 'It seems that you do not have html2ps installed! You have', 'to install it if you want to generate the PDF file'; return 0; # if you unbundle it make sure you write here a code similar to # can_create_pdf() } my $ps2pdf_exec = which('ps2pdf'); sub can_create_pdf { # check whether ps2pdf exists return $ps2pdf_exec if $ps2pdf_exec; print 'It seems that you do not have ps2pdf installed! You have', 'to install it if you want to generate the PDF file'; return 0; } sub scan_src_docs { my ($base, $ra_search_paths, $ra_search_exts) = @_; @search_paths = @{$ra_search_paths || []}; # .cfg is for matching config.cfg to become index.html %exts = map {$_ => 1} @{$ra_search_exts || []}, 'cfg'; my @ext_accept_pattern = map {quotemeta($_)."\$"} keys %exts; my $rsub_keep_ext = build_matchmany_sub(\@ext_accept_pattern); my %seen; for my $rel_path (@search_paths) { my $full_base_path = catdir $base, $rel_path; die "'search_paths' attr: $full_base_path is not a dir" unless -d $full_base_path; my @seen_pattern = map {"^".quotemeta($_)} keys %seen; my $rsub_skip_seen = build_matchmany_sub(\@seen_pattern); my $rel_uri = path2uri($rel_path); $src_docs{$rel_uri} = { # autogenerated index.html map { s/config\.cfg$/index.html/; ($_ => 1) } # full path => relative uri map path2uri( DocSet::Util::abs2rel($_, $full_base_path) ), grep $rsub_keep_ext->($_), # get files with wanted exts grep !$rsub_skip_seen->($_), # skip seen base dirs @{ expand_dir($full_base_path) } }; note "Scanning for src files: $full_base_path"; $seen{$full_base_path}++; } # dumper \%src_docs; } # this function returns a URI, so its separators are always / sub find_src_doc { my ($resource_rel_path) = @_; # push the html extension, because of autogenerated index.html, # which should be found automatically $exts{html} = 1 unless exists $exts{html}; for my $path (keys %src_docs) { if (my $found_path = match_in_doc_src_subset($path, $resource_rel_path)) { return path2uri($found_path); } } # if we didn't find anything so far, it's possible that the path was # specified with a longer prefix, that was needed (the above # searches only the end leaves), so try locate the segments of the # search path and search within maching sub-sets for my $path (@search_paths) { if ($resource_rel_path =~ m|^$path/(.*)|) { if (my $found_path = match_in_doc_src_subset($path, $1)) { return path2uri($found_path); } } } #dumper $src_docs{"docs/1.0"}; return; } # accepts the base_path (from the @search_paths) and the rel_path as # args, then it tries to find the match by applying known extensions. # # if matched, returns the whole path relative to the root, otherwise # returns undef sub match_in_doc_src_subset { my ($base_path, $rel_path) = @_; for my $ext (keys %exts) { #print qq{Try: $base_path :: $rel_path.$ext\n}; if (exists $src_docs{$base_path}{"$rel_path.$ext"}) { #print qq{Found $base_path/$rel_path.$ext\n}; return catdir $base_path, "$rel_path.$ext"; } } return; } # set render object: sort of Singleton, it'll complain aloud if the # object is set over the existing object, without first unsetting it sub set_render_obj { Carp::croak("usage: set_render_obj(\$obj) ") unless @_; Carp::croak("unset render_obj before setting a new one") if $render_obj; Carp::croak("undefined render_obj passed") unless defined $_[0]; $render_obj = shift; } sub get_render_obj { Carp::croak("render_obj is not available") unless $render_obj; return $render_obj; } sub unset_render_obj { Carp::croak("render_obj is not set") unless $render_obj; undef $render_obj; } 1; __END__ =head1 NAME C - RunTime Configuration =head1 SYNOPSIS use DocSet::RunTime; # run time options DocSet::RunTime::set_opt(\%args); if (get_opts('verbose') { print "verbose mode"; } # hosting system capabilities testing DocSet::RunTime::has_storable_module(); DocSet::RunTime::can_create_ps(); DocSet::RunTime::can_create_pdf(); # source documents lookup DocSet::RunTime::scan_src_docs($base_path, \@search_paths, \@search_exts); my $full_src_path = find_src_doc($resource_rel_path); # rendering object singleton set_render_obj($obj); unset_render_obj(); $obj = get_render_obj(); =head1 DESCRIPTION This module is a part of the docset application, and it stores the run time arguments, caches results of expensive calls and provide Singleton-like service to the whole system. =head1 FUNCTIONS META: To be completed, see SYNOPSIS =head2 Run Time Options Only get_opts() method is exported by default. =over =item * registers_reset() This function resets various run-time registers, used for validations. If the runtime is run more than once remember to always run first this function and even better always run it before using the runtime. e.g.: DocSet::RunTime::registers_reset(); my $docset = DocSet::DocSet::HTML->new($config_file); $docset->set_dir(abs_root => "."); $docset->scan; $docset->render; =item * register my @entries = register($register_name, $key, $val); Push into the register for a given key the supplied value. Return an array of the given register's key. For example used to track duplicated docset ids with: my @entries = DocSet::RunTime::register('unique_docset_id', $id, $self->{config_file}); die if @entries > 1; because if the register returns two value for the same key, someone has already registered that key before. The values in C<@entries> include the config files in this example. =item * set_opt(\%args) =item * get_opts() =back =head2 Hosting System Capabilities Testing These methods test the capability of the system and are a part of the runtime system to perform the checking only once. =over =item * has_storable_module =item * can_create_ps =item * can_create_pdf =back =head2 Source Documents Lookup A system for mapping L<> escapes to the located of the rendered files. This system scans once the C<@search_paths> for files with C<@search_exts> starting from C<$base_path> using scan_src_docs(). The C<@search_paths> and C<@search_exts> are configured in the I file. For example: dir => { # search path for pods, etc. must put more specific paths first! search_paths => [qw( foo/bar foo . )], # what extensions to search for search_exts => [qw(pod pm html)], }, So for example if the base path is I<~/myproject/src>, the files with extensions I<.pod>, I<.pm> and I<.html> will be searched in I<~/myproject/src/foo/bar>, I<~/myproject/src/foo> and I<~/myproject/src>. Notice that you must specify more specific paths first, since for optimization the seen paths are skipped. Therefore in our example the more explicit path I was listed before the more general I. When the POD parser finds a L<> sequence it indentifies the resource part and passes it to the find_src_doc() which looks up for this file in the cache and returns its original (src) location, which can be then easily converted to the final location and optionally adjusting the extension, e.g. when the POD file is converted to HTML. As a special extension this function automatically assumes that C will be generated in each directory containing items of an interest. Therefore in find_src_doc() it'll automatically find things like: L, even though there was no source I or I in first place. Only the find_src_doc() function is exported by default. =over =item * scan_src_docs($base_path, \@search_paths, \@search_exts); =item * find_src_doc($resource_rel_path); returns C if nothing was found. See the description above. =back =head2 Rendering Object Singleton Since the rendering process may happen by a third party system, into which we provide hooks or overload some of its methods, it's quite possible that we won't be able to access the current document (or better rendering) object. One solution would be to have a global package variable, but that's very error-prone. Therefore the used solution is to provide a hook into a RunTime environment setting the current rendering object when the rendering of a single page starts via C and unsetting it when it's finished via unset_render_obj(). Between these two moments the current rendering object can be retrieved with get_render_obj() method. Notice that this is all possible in the program which is not threaded, or/and only one rendering process exists at any given time from its start to its end. All three methods are exported by default. =over =item * set_render_obj($obj) Sets the current rendering object. You cannot set a new rendering object before the previous one is unset. This is in order to make sure that one document won't use by mistake a rendering object of another document. So when the rendering is done remember to call the unset_render_obj() function. =item * unset_render_obj() Unsets the currently set rendering object. =item * get_render_obj() Retrieves the currently set rendering object or complains aloud if it cannot find one. =back =head1 AUTHORS Stas Bekman Estas (at) stason.orgE =cut