--- /dev/null
+Revision history for Perl extension Zeal.
+
+0.000_001 2014-12-28T23:42+02:00
+ - Initial release
--- /dev/null
+Changes
+lib/Zeal.pm
+lib/Zeal/Docset.pm
+lib/Zeal/Document.pm
+Makefile.PL
+MANIFEST
+README
+t/ds/a.docset/Contents/Info.plist
+t/ds/a.docset/Contents/Resources/docSet.dsidx
+t/ds/a.docset/Contents/Resources/Documents/about.html
+t/ds/a.docset/Contents/Resources/Documents/array.html
+t/ds/b.docset/Contents/Info.plist
+t/ds/b.docset/Contents/Resources/docSet.dsidx
+t/ds/b.docset/Contents/Resources/Documents/building.html
+t/ds/b.docset/Contents/Resources/Documents/builtin.html
+t/ds/c.docset/Contents/Info.plist
+t/ds/c.docset/Contents/Resources/docSet.dsidx
+t/ds/c.docset/Contents/Resources/Documents/category.html
+t/ds/c.docset/Contents/Resources/Documents/class.html
+t/ds/dummy-file
+t/mkindex.pl
+t/perlcritic.t
+t/perlcriticrc
+t/Zeal.t
--- /dev/null
+use 5.014000;
+use ExtUtils::MakeMaker;
+
+WriteMakefile(
+ NAME => 'Zeal',
+ VERSION_FROM => 'lib/Zeal.pm',
+ ABSTRACT_FROM => 'lib/Zeal.pm',
+ AUTHOR => 'Marius Gavrilescu <marius@ieval.ro>',
+ MIN_PERL_VERSION => '5.14.0',
+ LICENSE => 'perl',
+ SIGN => 1,
+ PREREQ_PM => {
+ qw/DBI 0
+ DBD::SQLite 0
+ File::Slurp 0
+ Mac::PropertyList::SAX 0/,
+ },
+ META_ADD => {
+ dynamic_config => 0,
+ resources => {
+ repository => 'http://git.ieval.ro/?p=zeal.git',
+ },
+ }
+);
--- /dev/null
+Zeal version 0.000_001
+======================
+
+Dash is an offline API documentation browser. Zeal.pm is a module for
+reading and querying Dash documentation sets.
+
+For more information see the POD and the Dash website:
+http://kapeli.com/dash
+
+INSTALLATION
+
+To install this module type the following:
+
+ perl Makefile.PL
+ make
+ make test
+ make install
+
+DEPENDENCIES
+
+This module requires these other modules and libraries:
+
+* DBI
+* DBD::SQLite
+* File::Slurp
+* Mac::PropertyList::SAX
+
+COPYRIGHT AND LICENCE
+
+Copyright (C) 2014 by Marius Gavrilescu
+
+This library is free software; you can redistribute it and/or modify
+it under the same terms as Perl itself, either Perl version 5.20.1 or,
+at your option, any later version of Perl 5 you may have available.
+
+
--- /dev/null
+package Zeal;
+
+use 5.014000;
+use strict;
+use warnings;
+use re '/s';
+
+our $VERSION = '0.000_001';
+
+use File::Spec::Functions qw/catfile/;
+
+use Zeal::Docset;
+use Zeal::Document;
+
+sub new {
+ my ($class, $path) = @_;
+ $path //= $ENV{ZEAL_PATH};
+ my $self = bless {sets => {}}, $class;
+ if ($path) {
+ $self->add($_) for split /:/, $path;
+ }
+ $self
+}
+
+sub add {
+ my ($self, $path) = @_;
+ return unless -d $path;
+ if ($path =~ /[.]docset$/) {
+ my $ds = Zeal::Docset->new($path);
+ $self->{sets}{$ds->family} //= [];
+ push @{$self->{sets}{$ds->family}}, $ds;
+ } else {
+ my $dir;
+ opendir $dir, $path;
+ my @entries = grep { !/^[.]{1,2}$/ } readdir $dir;
+ closedir $dir;
+ $self->add(catfile $path, $_) for @entries
+ }
+}
+
+sub sets {
+ my ($self, $family) = @_;
+ return map { @$_ } values %{$self->{sets}} unless $family;
+ @{$self->{sets}{$family}}
+}
+
+sub query {
+ my ($self, $query, $family) = @_;
+ ($family, $query) = split /:/, $query, 2 if !$family && $query =~ /:/;
+ my @res = map { $_->query($query) } $self->sets($family);
+ wantarray ? @res : $res[0]
+}
+
+1;
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+Zeal - Read and query Dash/Zeal docsets
+
+=head1 SYNOPSIS
+
+ use Zeal;
+ my $zeal = Zeal->new("/home/mgv/docsets/:/home/mgv/something.docset");
+ # Add another docset
+ $zeal->add('/home/mgv/somethingelse.docset');
+ # Add a directory containing docsets
+ $zeal->add('/home/mgv/moredocsets/');
+ # Documentation for 'length' in all docsets
+ my $doc = $zeal->query('length');
+ # Documentation for all Test:: perl modules
+ my @docs = $zeal->query('Test::%', 'perl');
+ # Alternative syntax
+ @docs = $zeal->query('perl:Test::%);
+
+=head1 DESCRIPTION
+
+Dash is an offline API documentation browser. Zeal.pm is a module for
+reading and querying Dash documentation sets.
+
+This module queries multiple docsets. If you only have one docset, you
+should use the L<Zeal::Docset> module directly.
+
+Available methods:
+
+=over
+
+=item Zeal->B<new>([I<$path>])
+
+Create a new Zeal object. I<$path> is an optional colon delimited
+string for initializing the object. Each of its components is
+recursively scanned for docsets (and can also be a docset itself). If
+<$path> is not provided, the value of I<$ENV{ZEAL_PATH>> (if defined)
+is used instead.
+
+=item $zeal->B<add>(I<$path>)
+
+Recursively scan a path for docsets, adding them to this object.
+
+=item $zeal->B<sets>([I<$family>])
+
+Return a list of docsets (L<Zeal::Docset> objects) in the given
+family, or in all families if I<$family> is not provided.
+
+=item $zeal->B<query>(I<"$family:$query">)
+
+=item $zeal->B<query>(I<$query>, [I<$family>])
+
+Return a list of documents (L<Zeal::Document> objects) matching a
+query, optionally restricted to a family. In scalar context only one
+such document is returned. I<$query> is a SQL LIKE condition.
+
+=back
+
+=head1 SEE ALSO
+
+L<http://kapeli.com/dash>, L<http://zealdocs.org>
+
+=head1 AUTHOR
+
+Marius Gavrilescu, E<lt>marius@ieval.roE<gt>
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright (C) 2014 by Marius Gavrilescu
+
+This library is free software; you can redistribute it and/or modify
+it under the same terms as Perl itself, either Perl version 5.20.1 or,
+at your option, any later version of Perl 5 you may have available.
+
+
+=cut
--- /dev/null
+package Zeal::Docset;
+
+use 5.014000;
+use strict;
+use warnings;
+
+our $VERSION = '0.000_001';
+
+use parent qw/Class::Accessor::Fast/;
+__PACKAGE__->mk_ro_accessors(qw/path plist dbh name id family/);
+
+use Carp qw/carp/;
+use Cwd qw/realpath/;
+use File::Spec::Functions qw/catfile catdir rel2abs/;
+use HTTP::Tiny;
+
+use DBI;
+use File::Slurp qw/read_file/;
+use Mac::PropertyList::SAX qw/parse_plist_file/;
+
+use Zeal::Document;
+
+sub new {
+ my ($class, $path) = @_;
+ $path = realpath $path;
+ my $plpath = catfile $path, 'Contents', 'Info.plist';
+ my $dbpath = catfile $path, 'Contents', 'Resources', 'docSet.dsidx';
+ my $plist = parse_plist_file($plpath)->as_perl;
+ carp 'This is not a Dash docset' unless $plist->{isDashDocset};
+
+ bless {
+ path => $path,
+ plist => $plist,
+ dbh => DBI->connect("dbi:SQLite:dbname=$dbpath", '', ''),
+ name => $plist->{CFBundleName},
+ id => $plist->{CFBundleIdentifier},
+ family => $plist->{DocSetPlatformFamily},
+ }, $class
+}
+
+sub _blessdocs {
+ my ($self, $docsref) = @_;
+ map { Zeal::Document->new(+{%$_, docset => $self}) } @$docsref;
+}
+
+sub fetch {
+ my ($self, $path) = @_;
+ return HTTP::Tiny->new->get($path) if $path =~ /^http:/s;
+ my $docroot = catdir $self->path, 'Contents', 'Resources', 'Documents';
+ $path = rel2abs $path, $docroot;
+ scalar read_file $path
+}
+
+sub query {
+ my ($self, $cond) = @_;
+ my $query = 'SELECT * FROM searchIndex WHERE name LIKE ?';
+ my $res = $self->dbh->selectall_arrayref($query, {Slice => {}}, $cond);
+ my @results = $self->_blessdocs($res);
+ wantarray ? @results : $results[0]
+}
+
+sub get {
+ my ($self, $cond) = @_;
+ $self->query($cond)->fetch
+}
+
+sub list {
+ my ($self) = @_;
+ my $query = 'SELECT * FROM searchIndex';
+ my $res = $self->dbh->selectall_arrayref($query, {Slice => {}});
+ $self->_blessdocs($res)
+}
+
+1;
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+Zeal::Docset - Class representing a Dash/Zeal docset
+
+=head1 SYNOPSIS
+
+ use Zeal::Docset;
+
+=head1 DESCRIPTION
+
+Dash is an offline API documentation browser. Zeal::Docset is a class
+representing a Dash/Zeal docset.
+
+Available methods:
+
+=over
+
+=item Zeal::Docset->B<new>(I<$path>)
+
+Create a Zeal::Docset object from a given docset. I<$path> should be
+the path to a F<something.docset> directory.
+
+=item $ds->B<path>
+
+The path to the docset folder.
+
+=item $ds->B<plist>
+
+A hashref with the contents of Info.plist.
+
+=item $ds->B<dbh>
+
+A DBI database handle to the docSet.dsidx index.
+
+=item $ds->B<name>
+
+The name of this docset. Equivalent to
+C<< $ds->plist->{CFBundleName} >>
+
+=item $ds->B<id>
+
+The identifier of this docset. Equivalent to
+C<< $ds->plist->{CFBundleIdentifier} >>
+
+=item $ds->B<family>
+
+The family this docset belongs to. Dash uses this as the keyword for
+restricting searches to a particular family of docsets. Equivalent to
+C<< $ds->plist->{DocSetPlatformFamily} >>
+
+=item $ds->B<fetch>(I<$path>)
+
+Internal method for fetching the HTML content of a document. I<$path>
+is either the path to the document relative to C<< $ds->B<path> >> or
+a HTTP URL.
+
+=item $ds->B<query>(I<$cond>)
+
+In list context, return all documents (L<Zeal::Document> instances)
+matching I<$cond>. In scalar context, return one such document.
+I<$cond> is a SQL LIKE condition.
+
+=item $ds->B<get>(I<$cond>)
+
+The HTML content of one document that matches I<$cond>.
+I<$cond> is a SQL LIKE condition.
+
+This method is shorthand for C<< $ds->query(I<$cond>)->fetch >>.
+
+=item $ds->B<list>
+
+The list of all documents (L<Zeal::Document> instances) in this
+docset.
+
+=back
+
+=head1 SEE ALSO
+
+L<Zeal>, L<http://kapeli.com/dash>, L<http://zealdocs.org>
+
+=head1 AUTHOR
+
+Marius Gavrilescu, E<lt>marius@ieval.roE<gt>
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright (C) 2014 by Marius Gavrilescu
+
+This library is free software; you can redistribute it and/or modify
+it under the same terms as Perl itself, either Perl version 5.20.1 or,
+at your option, any later version of Perl 5 you may have available.
+
+
+=cut
--- /dev/null
+package Zeal::Document;
+
+use 5.014000;
+use strict;
+use warnings;
+
+our $VERSION = '0.000_001';
+
+use parent qw/Class::Accessor::Fast/;
+__PACKAGE__->mk_accessors(qw/id name type path docset/);
+
+sub fetch {
+ my ($self) = @_;
+ $self->docset->fetch($self->path)
+}
+
+1;
+__END__
+
+=encoding utf-8
+
+=head1 NAME
+
+Zeal::Document - Class representing a Dash/Zeal document
+
+=head1 SYNOPSIS
+
+ use Zeal::Document;
+
+=head1 DESCRIPTION
+
+Dash is an offline API documentation browser. Zeal::Document is a class
+representing a Dash/Zeal document.
+
+Available methods:
+
+=over
+
+=item $doc->B<id>
+
+The ID of this document. Not typically interesting.
+
+=item $doc->B<name>
+
+The name of this document.
+
+=item $doc->B<type>
+
+The type of this document. The list of types is available on
+the Dash website: L<http://kapeli.com/docsets#supportedentrytypes>
+
+=item $doc->B<path>
+
+The path of this document, relative to
+F<docset_root/Contents/Resources/Documents/>. This can also be a HTTP
+URL.
+
+=item $doc->B<fetch>
+
+The HTML content of this document, retrieved from the file system or
+via HTTP::Tiny.
+
+=back
+
+=head1 SEE ALSO
+
+L<Zeal>, L<http://kapeli.com/dash>, L<http://zealdocs.org>
+
+=head1 AUTHOR
+
+Marius Gavrilescu, E<lt>marius@ieval.roE<gt>
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright (C) 2014 by Marius Gavrilescu
+
+This library is free software; you can redistribute it and/or modify
+it under the same terms as Perl itself, either Perl version 5.20.1 or,
+at your option, any later version of Perl 5 you may have available.
+
+
+=cut
--- /dev/null
+#!/usr/bin/perl
+use strict;
+use warnings;
+
+use Test::More tests => 18;
+BEGIN { use_ok('Zeal') };
+
+note 'Working with t/ds/b.docset';
+my $ds = Zeal::Docset->new('t/ds/b.docset');
+is $ds->name, 'B', 'docset name is B';
+is $ds->id, 'b', 'docset id is b';
+is $ds->family, 'consonants', 'docset family is consonants';
+
+my @results = $ds->query('buil%');
+is @results, 2, 'query(buil%) returns two results';
+my $doc = $ds->query('building');
+is $doc->name, 'building', 'document name is building';
+is $doc->type, 'Word', 'document type is Word';
+like $doc->fetch, qr/^Dummy/, 'document HTML starts with "Dummy"';
+
+@results = sort {$a->name cmp $b->name} $ds->list;
+is @results, 2, 'docset contains two documents';
+is $results[0]->name, 'building', 'first result is "building"';
+
+note 'Working with all docsets in t/ds/';
+my $zeal = Zeal->new('t/ds/');
+my @sets = $zeal->sets;
+is @sets, 3, '3 docsets loaded';
+@results = $zeal->query('buil%');
+is @results, 2, '2 documents begin with "buil"';
+@results = $zeal->query('%t%');
+is @results, 3, '3 documents contain letter t';
+@results = $zeal->query('consonants:%t%');
+is @results, 2, '2 documents from the consonants family contain letter t';
+@results = $zeal->query('%t%', 'vowels');
+is @results, 1, '1 document from the vowels family contain letter t';
+
+note 'Working with t/ds/a.docset via Zeal and ->add';
+$zeal = Zeal->new;
+$zeal->add('t/ds/a.docset');
+@sets = $zeal->sets;
+is @sets, 1, '1 docset loaded';
+like $sets[0]->get('abou%'), qr/word/, 'HTML for abou% contains the word "word"';
+
+note 'Working with t/ds/a.docset via ZEAL_PATH';
+$ENV{ZEAL_PATH} = 't/ds/a.docset';
+$zeal = Zeal->new;
+is $zeal->query('about')->name, 'about';
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<plist version="1.0">
+<dict>
+<key>CFBundleIdentifier</key>
+<string>a</string>
+<key>CFBundleName</key>
+<string>A</string>
+<key>DocSetPlatformFamily</key>
+<string>vowels</string>
+<key>isDashDocset</key><true/></dict>
+</plist>
+
--- /dev/null
+About is a word beginning with the letter a.
--- /dev/null
+Array is a word beginning with the letter a
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<plist version="1.0">
+<dict>
+<key>CFBundleIdentifier</key>
+<string>b</string>
+<key>CFBundleName</key>
+<string>B</string>
+<key>DocSetPlatformFamily</key>
+<string>consonants</string>
+<key>isDashDocset</key><true/></dict>
+</plist>
+
--- /dev/null
+Dummy text
--- /dev/null
+Even more dummy text
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<plist version="1.0">
+<dict>
+<key>CFBundleIdentifier</key>
+<string>c</string>
+<key>CFBundleName</key>
+<string>C</string>
+<key>DocSetPlatformFamily</key>
+<string>consonants</string>
+<key>isDashDocset</key><true/></dict>
+</plist>
+
--- /dev/null
+Nothing of interest here
--- /dev/null
+This document intentionally left blank
--- /dev/null
+This is a dummy file
--- /dev/null
+#!/usr/bin/perl
+use v5.14;
+use warnings;
+
+use Cwd qw/getcwd/;
+use File::Find;
+use File::Spec::Functions qw/abs2rel/;
+use Zeal::Docset;
+
+sub mkindex {
+ my ($root) = @_;
+ my $oldwd = getcwd;
+ unlink "$root/Contents/Resources/docSet.dsidx";
+
+ my $ds = Zeal::Docset->new($root);
+ $ds->dbh->do('CREATE TABLE searchIndex(id INTEGER PRIMARY KEY, name TEXT, type TEXT, path TEXT)');
+ $ds->dbh->do('CREATE UNIQUE INDEX anchor on searchIndex (name, type, path)');
+
+ chdir "$root/Contents/Resources/Documents";
+
+ find +{
+ no_chdir => 1,
+ wanted => sub {
+ return unless -f;
+ my ($name) = m/(\w+)\.html/;
+ my $path = abs2rel $_;
+ say STDERR "Adding document $name at $path";
+ $ds->dbh->do('INSERT OR IGNORE INTO searchIndex(name, type, path) VALUES (?, \'Word\', ?)', {}, $name, $path);
+ }
+ }, '.';
+ chdir $oldwd;
+}
+
+mkindex $_ for <ds/*>
--- /dev/null
+#!/usr/bin/perl
+use v5.14;
+use warnings;
+
+use Test::More;
+
+BEGIN { plan skip_all => '$ENV{RELEASE_TESTING} is false' unless $ENV{RELEASE_TESTING} }
+use Test::Perl::Critic -profile => 't/perlcriticrc';
+
+all_critic_ok
--- /dev/null
+severity = 1
+
+[-BuiltinFunctions::ProhibitComplexMappings]
+[-CodeLayout::RequireTidyCode]
+[-ControlStructures::ProhibitPostfixControls]
+[-ControlStructures::ProhibitUnlessBlocks]
+[-Documentation::PodSpelling]
+[-Documentation::RequirePodLinksIncludeText]
+[-InputOutput::RequireBracedFileHandleWithPrint]
+[-References::ProhibitDoubleSigils]
+[-RegularExpressions::ProhibitEnumeratedClasses]
+[-RegularExpressions::RequireLineBoundaryMatching]
+[-Subroutines::RequireFinalReturn]
+[-ValuesAndExpressions::ProhibitConstantPragma]
+[-ValuesAndExpressions::ProhibitEmptyQuotes]
+[-ValuesAndExpressions::ProhibitMagicNumbers]
+[-ValuesAndExpressions::ProhibitNoisyQuotes]
+[-Variables::ProhibitLocalVars]
+[-Variables::ProhibitPackageVars]
+[-Variables::ProhibitPunctuationVars]
+
+[RegularExpressions::RequireExtendedFormatting]
+minimum_regex_length_to_complain_about = 20
+
+[Documentation::RequirePodSections]
+lib_sections = NAME | SYNOPSIS | DESCRIPTION | AUTHOR | COPYRIGHT AND LICENSE
+script_sections = NAME | SYNOPSIS | DESCRIPTION | AUTHOR | COPYRIGHT AND LICENSE
+
+[Subroutines::RequireArgUnpacking]
+short_subroutine_statements = 5
+allow_subscripts = 1