File Coverage

blib/lib/App/adr2org.pm
Criterion Covered Total %
statement 78 85 91.7
branch 23 32 71.8
condition 13 24 54.1
subroutine 7 7 100.0
pod 2 2 100.0
total 123 150 82.0


line stmt bran cond sub pod time code
1             package App::adr2org;
2              
3             our $DATE = '2014-10-15'; # DATE
4             our $VERSION = '0.04'; # VERSION
5              
6 1     1   454 use 5.010001;
  1         2  
  1         30  
7 1     1   4 use strict;
  1         1  
  1         21  
8 1     1   3 use warnings;
  1         1  
  1         18  
9              
10 1     1   414 use Sort::ByExample;
  1         10926  
  1         5  
11              
12             require Exporter;
13             our @ISA = qw(Exporter);
14             our @EXPORT_OK = qw(adr2org org2adr);
15              
16             my $sorter = Sort::ByExample->sorter([
17             "ID",
18             "NAME",
19             "URL",
20             "CREATED",
21             "TARGET",
22             "MOVE_IS_COPY",
23             "SEPARATOR_ALLOWED",
24             "EXPANDED",
25             "ACTIVE",
26             "VISITED",
27             "DESCRIPTION",
28             "TRASH FOLDER",
29             "DELETABLE",
30             "PARTNERID",
31             "UNIQUEID",
32             ], sub { $a cmp $b });
33              
34             our %SPEC;
35              
36             $SPEC{adr2org} = {
37             v => 1.1,
38             summary => 'Convert Opera bookmarks (bookmarks.adr) to Org document',
39             description => <<'_',
40              
41             I want to keep my Opera browser bookmarks file (`~/.opera/bookmarks.adr`) under
42             git repository, so I can synchronize them between computers. There are a few
43             annoyances though: 1) When Opera saves bookmarks file, it remove symlinks, so
44             after I have to re-symlink the file to my git repo; 2) The ID field changes
45             sporadically, creating unnecessarily large diff and merge conflicts.
46              
47             This program (and its counterpart `convert-org-to-opera-bookmarks`) is an
48             alternative to keeping Opera bookmarks file under git. You convert to .org file,
49             put the .org file under git, and convert back to .adr. The advantage is that the
50             ID field is removed so the diff is smaller and conflict reduced. Also, you can
51             more conveniently edit using Emacs/other Org editor.
52              
53             Another alternative to this program is to use the Opera Link service from Opera
54             to synchronize your bookmarks (and a few other stuffs) between devices. But note
55             that Opera has closed some of its services in the past.
56              
57             _
58             args => {
59             input => {
60             summary => 'Opera addressbook file',
61             schema => 'str*',
62             cmdline_src => 'stdin_or_files',
63             pos => 0,
64             req => 1,
65             },
66             exclude_trash => {
67             schema => 'bool',
68             cmdline_aliases => { T=>{} },
69             },
70             },
71             };
72             sub adr2org {
73 1     1 1 8 my %args = @_;
74              
75 1         2 my $exclude_trash = $args{exclude_trash};
76              
77 1         2 my $in_trash;
78 1         1 my $cur_level = 1;
79 1         1 my @sections;
80             my @ct;
81 1         33 for (split /(\r?\n){2,}/, $args{input}) {
82 24 100       45 if (/^#(\w+)\r?\n(.+)/s) {
    100          
83 7         17 push @sections, [$1, $2, $_];
84             } elsif ($_ eq '-') {
85 4         6 push @sections, ["endfolder"];
86             } else {
87             # ignore, including preamble text
88             }
89             }
90 1         4 for my $section (@sections) {
91 11         12 my $sname = $section->[0];
92 11 50       16 next if $sname eq 'DELETED';
93 11 100       16 if ($sname eq 'endfolder') {
94 4         3 $cur_level--;
95 4 50       8 die "BUG: trying to decrease level to 0" if $cur_level <= 0;
96 4         4 next;
97             }
98 7         55 my %sfields = $section->[1] =~ /\s*([^=]+)=(.*)/g;
99 7 100       15 if ($sname eq 'FOLDER') {
    50          
100 4   50     7 my $name = $sfields{NAME} // '';
101 4 50 33     9 $in_trash = 1 if $name eq 'Trash' && $cur_level == 1;
102 4 100 66     14 $in_trash = 0 if $name ne 'Trash' && $cur_level == 1;
103 4 50 33     8 unless ($in_trash && $exclude_trash) {
104 4         8 push @ct, ("*" x $cur_level), " FOLDER: $name\n";
105 4         11 for (grep {!/^(ID|NAME)$/} $sorter->(keys %sfields)) {
  16         95  
106 8         16 push @ct, "- $_ :: $sfields{$_}\n";
107             }
108             }
109 4         9 $cur_level++;
110             } elsif ($sname eq 'URL') {
111 3   50     6 my $name = $sfields{NAME} // '';
112 3 50 33     18 unless ($in_trash && $exclude_trash) {
113 3         8 push @ct, ("*" x $cur_level), " URL: $name\n";
114 3         8 for (grep {!/^(ID|NAME)$/} $sorter->(keys %sfields)) {
  17         102  
115 11         23 push @ct, "- $_ :: $sfields{$_}\n";
116             }
117             }
118             } else {
119 0         0 warn "Unknown section '$sname', skipped";
120 0         0 next;
121             }
122             }
123 1         14 [200, "OK", join("", @ct)];
124             }
125              
126             $SPEC{org2adr} = {
127             v => 1.1,
128             summary => 'Convert back Org to Opera bookmarks (bookmarks.adr)',
129             description => <<'_',
130              
131             This program is the counterpart for `convert-opera-bookmarks-to-org`) to turn
132             back the Org document generated by that program back to Opera bookmarks .adr
133             format. See that program for more information.
134              
135             _
136             args => {
137             input => {
138             summary => 'Org document file',
139             schema => 'str*',
140             cmdline_src => 'stdin_or_files',
141             pos => 0,
142             req => 1,
143             },
144             },
145             };
146             sub org2adr {
147 1     1 1 3 my %args = @_;
148              
149 1         1 my $cur_level;
150             my @sections;
151 0         0 my @ct;
152              
153 1     1   697 no strict 'refs';
  1         2  
  1         360  
154 1         8 push @ct, "Opera Hotlist version 2.0 (generated by ".__PACKAGE__.
155 1   50     1 " version ".(${__PACKAGE__.'::VERSION'} // "dev").")\n";
156 1         2 push @ct, "Options: encoding = utf8, version=3\n\n";
157              
158 1         13 for (split /^(?=\*+ )/m, $args{input}) {
159 7         8 push @sections, $_;
160             }
161 1         1 my $prev_level;
162 1         2 my $id = 0;
163 1         2 for my $section (@sections) {
164 7 50       24 $section =~ /^(\*+) (\w+): ?(.*)/ or do {
165 0         0 warn "Unknown section, skipped: $section";
166 0         0 next;
167             };
168 7         13 my ($level, $type, $sname) = ($1, $2, $3);
169 7 50 66     18 if ($type ne 'FOLDER' && $type ne 'URL') {
170 0         0 warn "Unknown section type '$type', skipped";
171 0         0 next;
172             }
173 7 100       11 if ($type eq 'FOLDER') {
174 4         4 $level = length($level);
175 4 100 100     13 if (defined($prev_level) && $level <= $prev_level) {
176 1         2 for ($level .. $prev_level) {
177 2         3 push @ct, "-\n\n";
178             }
179             }
180 4         4 $prev_level = $level;
181             }
182 7         9 push @ct, "#$type\n";
183 7         10 push @ct, "\tID=", ++$id, "\n";
184 7         8 push @ct, "\tNAME=$sname\n";
185 7         37 my %sfields = $section =~ /^- (\w+) :: (.*)/mg;
186 7         17 for ($sorter->(keys %sfields)) {
187 19         93 push @ct, "\t$_=$sfields{$_}\n";
188             }
189 7         13 push @ct, "\n";
190             }
191 1 50       3 if (defined $prev_level) {
192 1         3 push @ct, "-\n\n" for 1..$prev_level;
193             }
194 1         13 [200, "OK", join("", @ct)];
195             }
196              
197              
198             1;
199             # ABSTRACT: Convert Opera bookmarks to Org (and vice versa)
200              
201             __END__
202              
203             =pod
204              
205             =encoding UTF-8
206              
207             =head1 NAME
208              
209             App::adr2org - Convert Opera bookmarks to Org (and vice versa)
210              
211             =head1 VERSION
212              
213             This document describes version 0.04 of App::adr2org (from Perl distribution App-adr2org), released on 2014-10-15.
214              
215             =head1 DESCRIPTION
216              
217             This distribution provides the following utilities:
218              
219             adr2org
220             org2adr
221              
222             =head1 FUNCTIONS
223              
224              
225             =head2 adr2org(%args) -> [status, msg, result, meta]
226              
227             Convert Opera bookmarks (bookmarks.adr) to Org document.
228              
229             I want to keep my Opera browser bookmarks file (C<~/.opera/bookmarks.adr>) under
230             git repository, so I can synchronize them between computers. There are a few
231             annoyances though: 1) When Opera saves bookmarks file, it remove symlinks, so
232             after I have to re-symlink the file to my git repo; 2) The ID field changes
233             sporadically, creating unnecessarily large diff and merge conflicts.
234              
235             This program (and its counterpart C<convert-org-to-opera-bookmarks>) is an
236             alternative to keeping Opera bookmarks file under git. You convert to .org file,
237             put the .org file under git, and convert back to .adr. The advantage is that the
238             ID field is removed so the diff is smaller and conflict reduced. Also, you can
239             more conveniently edit using Emacs/other Org editor.
240              
241             Another alternative to this program is to use the Opera Link service from Opera
242             to synchronize your bookmarks (and a few other stuffs) between devices. But note
243             that Opera has closed some of its services in the past.
244              
245             Arguments ('*' denotes required arguments):
246              
247             =over 4
248              
249             =item * B<exclude_trash> => I<bool>
250              
251             =item * B<input>* => I<str>
252              
253             Opera addressbook file.
254              
255             =back
256              
257             Return value:
258              
259             Returns an enveloped result (an array).
260              
261             First element (status) is an integer containing HTTP status code
262             (200 means OK, 4xx caller error, 5xx function error). Second element
263             (msg) is a string containing error message, or 'OK' if status is
264             200. Third element (result) is optional, the actual result. Fourth
265             element (meta) is called result metadata and is optional, a hash
266             that contains extra information.
267              
268             (any)
269              
270              
271             =head2 org2adr(%args) -> [status, msg, result, meta]
272              
273             Convert back Org to Opera bookmarks (bookmarks.adr).
274              
275             This program is the counterpart for C<convert-opera-bookmarks-to-org>) to turn
276             back the Org document generated by that program back to Opera bookmarks .adr
277             format. See that program for more information.
278              
279             Arguments ('*' denotes required arguments):
280              
281             =over 4
282              
283             =item * B<input>* => I<str>
284              
285             Org document file.
286              
287             =back
288              
289             Return value:
290              
291             Returns an enveloped result (an array).
292              
293             First element (status) is an integer containing HTTP status code
294             (200 means OK, 4xx caller error, 5xx function error). Second element
295             (msg) is a string containing error message, or 'OK' if status is
296             200. Third element (result) is optional, the actual result. Fourth
297             element (meta) is called result metadata and is optional, a hash
298             that contains extra information.
299              
300             (any)
301              
302             =head1 HOMEPAGE
303              
304             Please visit the project's homepage at L<https://metacpan.org/release/App-adr2org>.
305              
306             =head1 SOURCE
307              
308             Source repository is at L<https://github.com/perlancar/perl-App-adr2org>.
309              
310             =head1 BUGS
311              
312             Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=App-adr2org>
313              
314             When submitting a bug or request, please include a test-file or a
315             patch to an existing test-file that illustrates the bug or desired
316             feature.
317              
318             =head1 AUTHOR
319              
320             perlancar <perlancar@cpan.org>
321              
322             =head1 COPYRIGHT AND LICENSE
323              
324             This software is copyright (c) 2014 by perlancar@cpan.org.
325              
326             This is free software; you can redistribute it and/or modify it under
327             the same terms as the Perl 5 programming language system itself.
328              
329             =cut