File Coverage

blib/lib/Org/To/HTML.pm
Criterion Covered Total %
statement 82 184 44.5
branch 19 64 29.6
condition 9 31 29.0
subroutine 18 33 54.5
pod 3 22 13.6
total 131 334 39.2


line stmt bran cond sub pod time code
1             package Org::To::HTML;
2              
3             our $AUTHORITY = 'cpan:PERLANCAR'; # AUTHORITY
4             our $DATE = '2020-09-11'; # DATE
5             our $DIST = 'Org-To-HTML'; # DIST
6             our $VERSION = '0.233'; # VERSION
7              
8 2     2   4855 use 5.010001;
  2         13  
9 2     2   10 use strict;
  2         3  
  2         39  
10 2     2   10 use warnings;
  2         3  
  2         46  
11 2     2   3481 use Log::ger;
  2         160  
  2         10  
12              
13 2     2   511 use vars qw($VERSION);
  2         3  
  2         113  
14              
15 2     2   476 use File::Slurper qw(read_text write_text);
  2         15727  
  2         108  
16 2     2   1034 use HTML::Entities qw/encode_entities/;
  2         11466  
  2         147  
17 2     2   1151 use Org::Document;
  2         147003  
  2         96  
18              
19 2     2   20 use Moo;
  2         6  
  2         13  
20 2     2   732 use experimental 'smartmatch';
  2         7  
  2         18  
21             with 'Org::To::Role';
22             extends 'Org::To::Base';
23              
24             require Exporter;
25             our @ISA;
26             push @ISA, qw(Exporter);
27             our @EXPORT_OK = qw(org_to_html);
28              
29             has naked => (is => 'rw');
30             has html_title => (is => 'rw');
31             has css_url => (is => 'rw');
32             has inline_images => (is => 'rw');
33              
34             our %SPEC;
35             $SPEC{org_to_html} = {
36             v => 1.1,
37             summary => 'Export Org document to HTML',
38             description => <<'_',
39              
40             This is the non-OO interface. For more customization, consider subclassing
41             Org::To::HTML.
42              
43             _
44             args => {
45             source_file => {
46             summary => 'Source Org file to export',
47             schema => ['str' => {}],
48             },
49             source_str => {
50             summary => 'Alternatively you can specify Org string directly',
51             schema => ['str' => {}],
52             },
53             target_file => {
54             summary => 'HTML file to write to',
55             schema => ['str' => {}],
56             description => <<'_',
57              
58             If not specified, HTML string will be returned.
59              
60             _
61             },
62             include_tags => {
63             summary => 'Include trees that carry one of these tags',
64             schema => ['array' => {of => 'str*'}],
65             description => <<'_',
66              
67             Works like Org's 'org-export-select-tags' variable. If the whole document
68             doesn't have any of these tags, then the whole document will be exported.
69             Otherwise, trees that do not carry one of these tags will be excluded. If a
70             selected tree is a subtree, the heading hierarchy above it will also be selected
71             for export, but not the text below those headings.
72              
73             _
74             },
75             exclude_tags => {
76             summary => 'Exclude trees that carry one of these tags',
77             schema => ['array' => {of => 'str*'}],
78             description => <<'_',
79              
80             If the whole document doesn't have any of these tags, then the whole document
81             will be exported. Otherwise, trees that do not carry one of these tags will be
82             excluded. If a selected tree is a subtree, the heading hierarchy above it will
83             also be selected for export, but not the text below those headings.
84              
85             exclude_tags is evaluated after include_tags.
86              
87             _
88             },
89             html_title => {
90             summary => 'HTML document title, defaults to source_file',
91             schema => ['str' => {}],
92             },
93             css_url => {
94             summary => 'Add a link to CSS document',
95             schema => ['str' => {}],
96             },
97             naked => {
98             summary => 'Don\'t wrap exported HTML with HTML/HEAD/BODY elements',
99             schema => ['bool' => {}],
100             },
101             ignore_unknown_settings => {
102             schema => 'bool',
103             },
104             inline_images => {
105             summary => 'If set to true, will make link to an image filename into an <img> element instead of <a>',
106             schema => 'bool',
107             default => 1,
108             },
109             },
110             };
111             sub org_to_html {
112 2     2 1 26287 my %args = @_;
113              
114 2         5 my $doc;
115 2 50       5 if ($args{source_file}) {
    0          
116             $doc = Org::Document->new(
117             from_string => scalar read_text($args{source_file}),
118             ignore_unknown_settings => $args{ignore_unknown_settings},
119 2         7 );
120             } elsif (defined($args{source_str})) {
121 0         0 $doc = Org::Document->new(from_string => $args{source_str});
122             } else {
123 0         0 return [400, "Please specify source_file/source_str"];
124             }
125              
126             my $obj = ($args{_class} // __PACKAGE__)->new(
127             include_tags => $args{include_tags},
128             exclude_tags => $args{exclude_tags},
129             css_url => $args{css_url},
130             naked => $args{naked},
131             html_title => $args{html_title} // $args{source_file},
132 2   50     35887 inline_images => $args{inline_images} // 1,
      66        
      50        
133             );
134              
135 2         1495 my $html = $obj->export($doc);
136             #$log->tracef("html = %s", $html);
137 2 50       7 if ($args{target_file}) {
138 0         0 write_text($args{target_file}, $html);
139 0         0 return [200, "OK"];
140             } else {
141 2         17 return [200, "OK", $html];
142             }
143             }
144              
145             sub export_document {
146 4     4 1 11 my ($self, $doc) = @_;
147              
148 4         10 $self->{_prev_elem_is_inline} = 0;
149              
150 4         8 my $html = [];
151 4 100       20 unless ($self->naked) {
152 1         3 push @$html, "<html>\n";
153 1   50     56 push @$html, (
154             "<!-- Generated by ".__PACKAGE__,
155             " version ".($VERSION // "?"),
156             " on ".scalar(localtime)." -->\n\n");
157              
158 1         5 push @$html, "<head>\n";
159 1   50     6 push @$html, "<title>",
160             ($self->html_title // "(no title)"), "</title>\n";
161 1 50       5 if ($self->css_url) {
162 1         13 push @$html, (
163             "<link rel=\"stylesheet\" type=\"text/css\" href=\"",
164             $self->css_url, "\" />\n"
165             );
166             }
167 1         3 push @$html, "</head>\n\n";
168              
169 1         2 push @$html, "<body>\n";
170             }
171 4         15 push @$html, $self->export_elements(@{$doc->children});
  4         33  
172 4 100       14 unless ($self->naked) {
173 1         3 push @$html, "</body>\n\n";
174 1         4 push @$html, "</html>\n";
175             }
176              
177 4         26 join "", @$html;
178             }
179              
180             sub before_export_element {
181 21     21 1 29 my $self = shift;
182 21         61 my %args = @_;
183              
184             $self->{_prev_elem_is_inline} =
185 21 100 100     126 $args{elem}->can("is_inline") && $args{elem}->is_inline ? 1:0;
186             }
187              
188             sub export_block {
189 1     1 0 2 my ($self, $elem) = @_;
190             # currently all assumed to be <PRE>
191 1         9 join "", (
192             "<pre class=\"block block_", lc($elem->name), "\">",
193             encode_entities($elem->raw_content),
194             "</pre>\n\n"
195             );
196             }
197              
198             sub export_fixed_width_section {
199 1     1 0 4 my ($self, $elem) = @_;
200 1         3 join "", (
201             "<pre class=\"fixed_width_section\">",
202             encode_entities($elem->text),
203             "</pre>\n"
204             );
205             }
206              
207             sub export_comment {
208 0     0 0 0 my ($self, $elem) = @_;
209 0         0 join "", (
210             "<!-- ",
211             encode_entities($elem->_str),
212             " -->\n"
213             );
214             }
215              
216             sub export_drawer {
217 0     0 0 0 my ($self, $elem) = @_;
218             # currently not exported
219 0         0 '';
220             }
221              
222             sub export_footnote {
223 0     0 0 0 my ($self, $elem) = @_;
224             # currently not exported
225 0         0 '';
226             }
227              
228             sub export_headline {
229 5     5 0 12 my ($self, $elem) = @_;
230              
231 5         22 my @children = $self->_included_children($elem);
232              
233 5         39 join "", (
234             "<h" , $elem->level, ">",
235             $self->export_elements($elem->title),
236             "</h", $elem->level, ">\n\n",
237             $self->export_elements(@children)
238             );
239             }
240              
241             sub export_list {
242 0     0 0 0 my ($self, $elem) = @_;
243 0         0 my $tag;
244 0         0 my $type = $elem->type;
245 0 0       0 if ($type eq 'D') { $tag = 'dl' }
  0 0       0  
    0          
246 0         0 elsif ($type eq 'O') { $tag = 'ol' }
247 0         0 elsif ($type eq 'U') { $tag = 'ul' }
248             join "", (
249             "<$tag>\n",
250 0   0     0 $self->export_elements(@{$elem->children // []}),
  0         0  
251             "</$tag>\n\n"
252             );
253             }
254              
255             sub export_list_item {
256 0     0 0 0 my ($self, $elem) = @_;
257              
258 0         0 my $html = [];
259 0 0       0 if ($elem->desc_term) {
260 0         0 push @$html, "<dt>";
261             } else {
262 0         0 push @$html, "<li>";
263             }
264              
265 0 0       0 if ($elem->check_state) {
266 0         0 push @$html, "<strong>[", $elem->check_state, "]</strong>";
267             }
268              
269 0 0       0 if ($elem->desc_term) {
270 0         0 push @$html, $self->export_elements($elem->desc_term);
271 0         0 push @$html, "</dt>";
272 0         0 push @$html, "<dd>";
273             }
274              
275 0 0       0 push @$html, $self->export_elements(@{$elem->children}) if $elem->children;
  0         0  
276              
277 0 0       0 if ($elem->desc_term) {
278 0         0 push @$html, "</dd>\n";
279             } else {
280 0         0 push @$html, "</li>\n";
281             }
282              
283 0         0 join "", @$html;
284             }
285              
286             sub export_radio_target {
287 0     0 0 0 my ($self, $elem) = @_;
288             # currently not exported
289 0         0 '';
290             }
291              
292             sub export_setting {
293 1     1 0 4 my ($self, $elem) = @_;
294             # currently not exported
295 1         3 '';
296             }
297              
298             sub export_table {
299 0     0 0 0 my ($self, $elem) = @_;
300             join "", (
301             "<table border>\n",
302 0   0     0 $self->export_elements(@{$elem->children // []}),
  0         0  
303             "</table>\n\n"
304             );
305             }
306              
307             sub export_table_row {
308 0     0 0 0 my ($self, $elem) = @_;
309             join "", (
310             "<tr>",
311 0   0     0 $self->export_elements(@{$elem->children // []}),
  0         0  
312             "</tr>\n"
313             );
314             }
315              
316             sub export_table_cell {
317 0     0 0 0 my ($self, $elem) = @_;
318              
319             join "", (
320             "<td>",
321 0   0     0 $self->export_elements(@{$elem->children // []}),
  0         0  
322             "</td>"
323             );
324             }
325              
326             sub export_table_vline {
327 0     0 0 0 my ($self, $elem) = @_;
328             # currently not exported
329 0         0 '';
330             }
331              
332             sub __escape_target {
333 0     0   0 my $target = shift;
334 0         0 $target =~ s/[^\w]+/_/g;
335 0         0 $target;
336             }
337              
338             sub export_target {
339 0     0 0 0 my ($self, $elem) = @_;
340             # target
341 0         0 join "", (
342             "<a name=\"", __escape_target($elem->target), "\">"
343             );
344             }
345              
346             sub export_text {
347 8     8 0 17 my ($self, $elem) = @_;
348              
349 8         16 my $style = $elem->style;
350 8         13 my $tag;
351 8 50       48 if ($style eq 'B') { $tag = 'b' }
  0 50       0  
    50          
    50          
    50          
    50          
352 0         0 elsif ($style eq 'I') { $tag = 'i' }
353 0         0 elsif ($style eq 'U') { $tag = 'u' }
354 0         0 elsif ($style eq 'S') { $tag = 'strike' }
355 0         0 elsif ($style eq 'C') { $tag = 'code' }
356 0         0 elsif ($style eq 'V') { $tag = 'tt' }
357              
358 8         15 my $html = [];
359              
360 8 50       15 push @$html, "<$tag>" if $tag;
361 8         46 my $text = encode_entities($elem->text);
362 8         174 $text =~ s/\R\R+/\n\n<p>/g;
363 8 50       52 if ($self->{_prev_elem_is_inline}) {
364 8         41 $text =~ s/\A\R/ /;
365             }
366 8         29 $text =~ s/(?<=.)\R/ /g;
367 8         19 push @$html, $text;
368 8 50       26 push @$html, $self->export_elements(@{$elem->children}) if $elem->children;
  0         0  
369 8 50       15 push @$html, "</$tag>" if $tag;
370              
371 8         35 join "", @$html;
372             }
373              
374             sub export_time_range {
375 0     0 0   my ($self, $elem) = @_;
376              
377 0           encode_entities($elem->as_string);
378             }
379              
380             sub export_timestamp {
381 0     0 0   my ($self, $elem) = @_;
382              
383 0           encode_entities($elem->as_string);
384             }
385              
386             sub export_link {
387 0     0 0   require Filename::Image;
388 0           require URI;
389              
390 0           my ($self, $elem) = @_;
391              
392 0           my $html = [];
393 0           my $link = $elem->link;
394 0           my $looks_like_image = Filename::Image::check_image_filename(filename => $link);
395 0           my $inline_images = $self->inline_images;
396              
397 0 0 0       if ($inline_images && $looks_like_image) {
398             # TODO: extract to method e.g. settings
399 0           my $elem_settings;
400 0           my $s = $elem;
401 0           while (1) {
402 0           $s = $s->prev_sibling;
403 0 0 0       last unless $s && $s->isa("Org::Element::Setting");
404 0           $elem_settings->{ $s->name } = $s->raw_arg;
405             }
406             #use DD; dd $settings;
407 0           my $caption = $elem_settings->{CAPTION};
408              
409             # TODO: extract to method e.g. settings of Org::Document
410 0           my $doc_settings;
411 0           $s = $elem->document->children->[0];
412 0           while (1) {
413 0           $s = $s->next_sibling;
414 0 0 0       last unless $s && $s->isa("Org::Element::Setting");
415 0           $doc_settings->{ $s->name } = $s->raw_arg;
416             }
417             #use DD; dd $settings;
418 0           my $img_base = $doc_settings->{IMAGE_BASE};
419              
420 0 0         my $url = defined($img_base) ? URI->new($link)->abs(URI->new($img_base)) : $link;
421              
422 0 0         push @$html, "<figure>" if defined $caption;
423 0           push @$html, "<img src=\"";
424 0           push @$html, "$url";
425 0           push @$html, "\" />";
426 0           push @$html, "<figcaption>", encode_entities($caption), "</figcaption>";
427 0 0         push @$html, "</figure>" if defined $caption;
428             } else {
429 0           push @$html, "<a href=\"";
430 0           push @$html, $link;
431 0           push @$html, "\">";
432 0 0         if ($elem->description) {
433 0           push @$html, $self->export_elements($elem->description);
434             } else {
435 0           push @$html, $link;
436             }
437 0           push @$html, "</a>";
438             }
439              
440 0           join "", @$html;
441             }
442              
443             1;
444             # ABSTRACT: Export Org document to HTML
445              
446             __END__
447              
448             =pod
449              
450             =encoding UTF-8
451              
452             =head1 NAME
453              
454             Org::To::HTML - Export Org document to HTML
455              
456             =head1 VERSION
457              
458             This document describes version 0.233 of Org::To::HTML (from Perl distribution Org-To-HTML), released on 2020-09-11.
459              
460             =head1 SYNOPSIS
461              
462             use Org::To::HTML qw(org_to_html);
463              
464             # non-OO interface
465             my $res = org_to_html(
466             source_file => 'todo.org', # or source_str
467             #target_file => 'todo.html', # defaults return the HTML in $res->[2]
468             #html_title => 'My Todo List', # defaults to file name
469             #include_tags => [...], # default exports all tags.
470             #exclude_tags => [...], # behavior mimics emacs's include/exclude rule
471             #css_url => '/path/to/my/style.css', # default none
472             #naked => 0, # if set to 1, no HTML/HEAD/BODY will be output.
473             );
474             die "Failed" unless $res->[0] == 200;
475              
476             # OO interface
477             my $oeh = Org::To::HTML->new();
478             my $html = $oeh->export($doc); # $doc is Org::Document object
479              
480             =head1 DESCRIPTION
481              
482             Export Org format to HTML. To customize, you can subclass this module.
483              
484             A command-line utility L<org-to-html> is available in the distribution
485             L<App::OrgUtils>.
486              
487             Note that this module is just a simple exporter, for "serious" works you'll
488             probably want to use the exporting features or L<org-mode|http://orgmode.org>.
489              
490             =head1 new(%args)
491              
492             =head2 $exp->export_document($doc) => HTML
493              
494             Export document to HTML.
495              
496             =head1 FUNCTIONS
497              
498              
499             =head2 org_to_html
500              
501             Usage:
502              
503             org_to_html(%args) -> [status, msg, payload, meta]
504              
505             Export Org document to HTML.
506              
507             This is the non-OO interface. For more customization, consider subclassing
508             Org::To::HTML.
509              
510             This function is not exported by default, but exportable.
511              
512             Arguments ('*' denotes required arguments):
513              
514             =over 4
515              
516             =item * B<css_url> => I<str>
517              
518             Add a link to CSS document.
519              
520             =item * B<exclude_tags> => I<array[str]>
521              
522             Exclude trees that carry one of these tags.
523              
524             If the whole document doesn't have any of these tags, then the whole document
525             will be exported. Otherwise, trees that do not carry one of these tags will be
526             excluded. If a selected tree is a subtree, the heading hierarchy above it will
527             also be selected for export, but not the text below those headings.
528              
529             exclude_tags is evaluated after include_tags.
530              
531             =item * B<html_title> => I<str>
532              
533             HTML document title, defaults to source_file.
534              
535             =item * B<ignore_unknown_settings> => I<bool>
536              
537             =item * B<include_tags> => I<array[str]>
538              
539             Include trees that carry one of these tags.
540              
541             Works like Org's 'org-export-select-tags' variable. If the whole document
542             doesn't have any of these tags, then the whole document will be exported.
543             Otherwise, trees that do not carry one of these tags will be excluded. If a
544             selected tree is a subtree, the heading hierarchy above it will also be selected
545             for export, but not the text below those headings.
546              
547             =item * B<inline_images> => I<bool> (default: 1)
548              
549             If set to true, will make link to an image filename into an <imgE<gt> element instead of <aE<gt>.
550              
551             =item * B<naked> => I<bool>
552              
553             Don't wrap exported HTML with HTMLE<sol>HEADE<sol>BODY elements.
554              
555             =item * B<source_file> => I<str>
556              
557             Source Org file to export.
558              
559             =item * B<source_str> => I<str>
560              
561             Alternatively you can specify Org string directly.
562              
563             =item * B<target_file> => I<str>
564              
565             HTML file to write to.
566              
567             If not specified, HTML string will be returned.
568              
569              
570             =back
571              
572             Returns an enveloped result (an array).
573              
574             First element (status) is an integer containing HTTP status code
575             (200 means OK, 4xx caller error, 5xx function error). Second element
576             (msg) is a string containing error message, or 'OK' if status is
577             200. Third element (payload) is optional, the actual result. Fourth
578             element (meta) is called result metadata and is optional, a hash
579             that contains extra information.
580              
581             Return value: (any)
582              
583             =for Pod::Coverage ^(export_.+|before_.+|after_.+)$
584              
585             =head1 ATTRIBUTES
586              
587             =head2 naked => BOOL
588              
589             If set to true, export_document() will not output HTML/HEAD/BODY wrapping
590             element. Default is false.
591              
592             =head2 html_title => STR
593              
594             Title to use in TITLE element. If unset, defaults to "(no title)" when
595             exporting.
596              
597             =head2 css_url => STR
598              
599             If set, export_document() will output a LINK element pointing to this CSS.
600              
601             =head1 METHODS
602              
603             =head1 HOMEPAGE
604              
605             Please visit the project's homepage at L<https://metacpan.org/release/Org-To-HTML>.
606              
607             =head1 SOURCE
608              
609             Source repository is at L<https://github.com/perlancar/perl-Org-To-HTML>.
610              
611             =head1 BUGS
612              
613             Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=Org-To-HTML>
614              
615             When submitting a bug or request, please include a test-file or a
616             patch to an existing test-file that illustrates the bug or desired
617             feature.
618              
619             =head1 SEE ALSO
620              
621             For more information about Org document format, visit http://orgmode.org/
622              
623             L<Org::Parser>
624              
625             L<org-to-html>
626              
627             =head1 AUTHOR
628              
629             perlancar <perlancar@cpan.org>
630              
631             =head1 COPYRIGHT AND LICENSE
632              
633             This software is copyright (c) 2020, 2018, 2017, 2016, 2015, 2014, 2013, 2012, 2011 by perlancar@cpan.org.
634              
635             This is free software; you can redistribute it and/or modify it under
636             the same terms as the Perl 5 programming language system itself.
637              
638             =cut