File Coverage

blib/lib/Org/To/HTML.pm
Criterion Covered Total %
statement 89 191 46.6
branch 20 66 30.3
condition 9 36 25.0
subroutine 19 34 55.8
pod 3 22 13.6
total 140 349 40.1


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