File Coverage

blib/lib/Text/Slidez.pm
Criterion Covered Total %
statement 12 12 100.0
branch n/a
condition n/a
subroutine 4 4 100.0
pod n/a
total 16 16 100.0


line stmt bran cond sub pod time code
1             package Text::Slidez;
2             $VERSION = v0.0.1;
3              
4 1     1   1045 use warnings;
  1         3  
  1         43  
5 1     1   7 use strict;
  1         2  
  1         36  
6 1     1   17 use Carp;
  1         2  
  1         87  
7              
8 1     1   6 use base 'Shebangml';
  1         1  
  1         892  
9             use Class::Accessor::Classy;
10             lw 'slides';
11             no Class::Accessor::Classy;
12              
13             use XML::Bits qw(T);
14              
15             =head1 NAME
16              
17             Text::Slidez - format slideshows into XHTML
18              
19             =head1 SYNOPSIS
20              
21             See L for the command-line frontend.
22              
23             use Text::Slidez;
24              
25             my $slidez = Text::Slidez->new;
26             $slidez->load('my_slides.hbml');
27             foreach my $slide ($slidez->slides) {
28             ...
29             }
30              
31             =cut
32              
33              
34             =head2 load
35              
36             $slidez->load('my_slides.hbml');
37              
38             =cut
39              
40             sub load {
41             my $self = shift;
42             my $input = shift;
43              
44             local $self->{ctx};
45             local $self->{started};
46              
47             $self->process($input);
48              
49             # bit of cleanup on the innards:
50             foreach my $slide ($self->slides) {
51             my @kids =
52             grep({not ($_->tag eq '' and "$_" eq '')} $slide->children);
53             shift(@kids) while($kids[0] =~ m/^\s+$/);
54             pop(@kids) if($kids[-1] =~ m/^\n\s*$/);
55             $slide->{children} = [@kids];
56             }
57              
58             #warn join("\n---\n", @{$self->{slides}});
59             return($self);
60             } # load ###############################################################
61              
62             =head2 dump
63              
64             Dump a marked-up version of the raw data.
65              
66             warn $slidez->dump;
67              
68             =cut
69              
70             sub dump {
71             my $self = shift;
72             return join("\n---\n",
73             map({join("|", map({"($_)=" . $_->tag} $_->children))}
74             $self->slides)
75             ), "\n";
76             } # dump ###############################################################
77              
78             =head2 format_slide
79              
80             Format a single slide for output.
81              
82             my $xhtml = $slidez->format_slide($slide, %opts);
83              
84             =cut
85              
86             sub format_slide {
87             my $self = shift;
88             my ($slide, %opts) = @_;
89              
90             my @parts = $self->_part_slide($slide);
91              
92             # see if we can deduce a title from the first time we see one
93             unless($opts{title} or $self->{title}) {
94             if($parts[2] and @{$parts[1]} == 0) {
95             my $text = join('', @{$parts[0]});
96             ($text) = split(/\n/, $text);
97             $text =~ s/<[^>]+>//g;
98             $self->{title} = $text;
99             }
100             }
101              
102             my $page = T{html =>
103             T{head =>
104             T{title => $opts{title}||$self->{title}||'slidez'},
105             T{meta =>
106             ['http-equiv' => "Content-Type",
107             content => "text/html;charset=utf-8"]},
108             T{meta =>
109             ['http-equiv'=>"Content-Style-Type",
110             content => "text/css"]},
111             T{link =>
112             [rel=> 'stylesheet', href => 'style.css', type => 'text/css']},
113             T{script => [type => 'text/javascript'],
114             $self->_mk_script(%opts);
115             },
116             },
117             T{body =>}
118             };
119             $page->set_doctype('html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
120             "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"');
121              
122             my $div = $page->child(-1)->create_child(div => [class => 'slide']);
123              
124             $self->_handle_parts($div, \@parts,
125             calc_width => sub {
126             my $n = $self->_calc_width(shift);
127             $n > 20 ? '900px' : $n . 'em';
128             },
129             );
130              
131             return($page);
132            
133             } # format_slide #######################################################
134              
135             =head2 as_single_page
136              
137             $slidez->as_single_page;
138              
139             =cut
140              
141             sub as_single_page {
142             my $self = shift;
143              
144             my @slides = $self->slides;
145              
146             my $page = T{html =>
147             T{head =>
148             T{title => },
149             T{meta =>
150             ['http-equiv' => "Content-Type",
151             content => "text/html;charset=utf-8"]},
152             T{meta =>
153             ['http-equiv'=>"Content-Style-Type",
154             content => "text/css"]},
155             T{link =>
156             [rel=> 'stylesheet', href => 'style-flat.css', type => 'text/css']},
157             },
158             T{body =>}
159             };
160             $page->set_doctype('html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
161             "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"');
162              
163             my $title = $page->child(0)->child(0);
164             my $body = $page->child(-1);
165             my $outer = $body->create_child(div => [style=> "width: 600px"]);
166              
167             for my $i (0..$#slides) {
168             my $div = $outer->create_child(div => [class => 'slide']);
169              
170             my @parts = $self->_part_slide($slides[$i]);
171             unless($i) { # look for title on the first slide
172             if($parts[2] and @{$parts[1]} == 0) {
173             my $text = join('', @{$parts[0]});
174             ($text) = split(/\n/, $text);
175             $text =~ s/^\s+//;
176             $text =~ s/<[^>]+>//g;
177             $title->create_child(''=> $text);
178             }
179             }
180              
181             #warn "\n\nhandle $i\n\n\n";
182             $self->_handle_parts($div, \@parts,
183             calc_width => sub {
184             my $n = $self->_calc_width(shift);
185             $n > 20 ? '500px' : $n . 'em';
186             },
187             );
188              
189             $outer->create_child(div =>
190             [class => 'wee', style => "width:100%; text-align: right"],
191             )->create_child('' =>
192             '' => $i+1 . ' / ' . scalar(@slides));
193             $outer->create_child(hr =>);
194             }
195              
196             return($page);
197             } # as_single_page #####################################################
198              
199             my %span_map = (
200             L => 'large',
201             M => 'medium',
202             S => 'small',
203             );
204              
205             sub _atag {
206             my $self = shift;
207             my ($tag, $atts) = @_;
208              
209             my @attr = $atts ? $atts->atts : ();
210             if(my $class = $span_map{$tag}) {
211             $tag = 'span';
212             push(@attr, class => $class);
213             }
214              
215             my $el = XML::Bits->new($tag, @attr ? \@attr : ());
216              
217             if($self->{ctx}) {
218             croak("no nested slides") if($tag eq 'slide');
219             $self->{ctx}->add_child($el);
220             $self->{ctx} = $el;
221             }
222             else {
223             if($tag eq 'slide') {
224             croak("no start element") unless($self->{started});
225             my $sl = $self->{slides} ||= [];
226             $self->{ctx} = $el;
227             push(@$sl, $el);
228             }
229             elsif($tag eq 'slides') {
230             $self->{started} = 1;
231             }
232             else {
233             croak("content '$tag' outside of slide!");
234             }
235             }
236              
237             return($el);
238             }
239              
240             =head2 do_code
241              
242             $slidez->do_code($tag, $atts, $string);
243              
244             =cut
245              
246             sub do_code {
247             my $self = shift;
248             my ($tag, $atts, $string) = @_;
249              
250             my %atts = $atts ? $atts->atts : ();
251              
252             my $make = sub {
253             my $pre = $self->{ctx}->create_child(pre => [%atts]);
254             $pre->create_child('' => $_) for(@_);
255             };
256              
257             my $ft = delete($atts{type});
258              
259             require Text::VimColor;
260             my $cache;
261             if($string) {
262             # XXX how to do the caching?
263             # warn "string code is slow: $string\n";
264             }
265             else {
266             my $src = delete $atts{src} or croak("must have src");
267             my $input = File::Fu->file($src);
268             my $cache_dir = File::Fu->dir('.cache');
269             if($cache_dir->d) {
270             $cache = $cache_dir + $input->file;
271             if($cache->e and $cache->stat->mtime >= $input->stat->mtime) {
272             warn "load $input from cache\n";
273             return($make->(scalar $cache->read));
274             }
275             }
276             my %ftmap = (
277             html => 'html',
278             hbml => 'hbml',
279             pl => 'perl',
280             pm => 'perl',
281             );
282             unless($ft) {
283             my ($ext) = $input =~ m/\.([^\.]+)$/;
284             $ft = $ftmap{$ext} if($ftmap{$ext});
285             }
286             $string = $input->read;
287             }
288             my $html = Text::VimColor->new(
289             string => $string,
290             $ft ? (filetype => $ft) : (),
291             )->html;
292              
293             # leading whitespace cleanup
294             $html =~ s{]*>(\s+)}{$1}g;
295             # pull whitespace out front
296             $html =~ s{^(]+>)(\s+)}{$2$1}mg;
297             $html =~ s/\n+$//;
298             $make->($html);
299             $cache->write($html) if($cache);
300            
301             } # do_code ############################################################
302              
303             =head1 Shebangml Callbacks
304              
305             These are really part of the parser class and not the API.
306              
307             =head2 put_tag
308              
309             $slidez->put_tag($tag, $atts, $string);
310              
311             =cut
312              
313             sub put_tag {
314             my $self = shift;
315             my ($tag, $atts, $string) = @_;
316              
317             return $self->do_code(@_) if($tag eq 'code');
318             return $self->do_include($atts) if($tag eq '.include');
319              
320             my $el = $self->_atag($tag, $atts);
321              
322             $el->create_child('' => $self->escape_text($string))
323             if(defined($string));
324              
325             $self->{ctx} = $el->parent;
326              
327             } # put_tag ############################################################
328              
329             =head2 put_tag_start
330              
331             $slidez->put_tag_start($tag, $atts);
332              
333             =cut
334              
335             sub put_tag_start {
336             my $self = shift;
337             my ($tag, $atts) = @_;
338              
339             my $el = $self->_atag($tag, $atts);
340              
341             } # put_tag_start ######################################################
342              
343             =head2 put_tag_end
344              
345             $slidez->put_tag_end($tag);
346              
347             =cut
348              
349             sub put_tag_end {
350             my $self = shift;
351             my ($tag) = @_;
352              
353             $tag = 'span' if($span_map{$tag});
354              
355             my $ctx = delete($self->{ctx});
356             return() if($tag eq 'slides');
357             ($ctx->tag eq $tag) or croak($ctx->tag, " is not a $tag!");
358             croak("context fail $tag")
359             unless($self->{ctx} = $ctx->parent or $tag eq 'slide');
360              
361             } # put_tag_end ########################################################
362              
363             =head2 put_text
364              
365             $slidez->put_text($text);
366              
367             =cut
368              
369             sub put_text {
370             my $self = shift;
371             my ($text) = @_;
372              
373             my $ctx = $self->{ctx} or return;
374             $ctx->create_child('',
375             length($text) ? $self->escape_text($text) : '');
376             # TODO escaped text might actually contain some certain tags :-/
377              
378             } # put_text ###########################################################
379              
380             =head2 _part_slide
381              
382             my @parts = $self->_part_slide($slide);
383              
384             =cut
385              
386             sub _part_slide {
387             my $self = shift;
388             my ($slide) = @_;
389              
390             my @children = $slide->children;
391             pop(@children) if($children[-1] =~ m/^\s*$/);
392             my @parts = ([]);
393             my $sp;
394             # warn join(",", map({$_->type} @children));
395             # if($children[0]->is_text) { # undenting :-/
396             # $children[0]->{content} =~ s/^(\s+)//;
397             # $sp = $1;
398             # }
399             # warn "sp is >$sp<\n";
400             while(@children) {
401             my $bit = shift(@children);
402             if($bit->is_text and $bit->{content} =~ s/\n$//) {
403             #$bit->{content} =~ s/^$sp// if(defined($sp));
404             push(@{$parts[-1]}, $bit) if(length($bit));
405             push(@parts, []); # start a new group
406             }
407             else {
408             #if($bit->is_text) { $bit->{content} =~ s/^$sp// if(defined($sp)); }
409             push(@{$parts[-1]}, $bit);
410             }
411             }
412              
413             foreach my $part (@parts) {
414             next unless(@$part);
415             shift(@$part)
416             while($part->[0]->is_text and $part->[0] =~ m/^\s+$/);
417             }
418              
419             # drop the trailing chunk
420             pop(@parts) if(@{$parts[-1]} == 0);
421              
422             if(0) {
423             warn "slide:\n";
424             warn join("\n---\n", map({join('|', @$_)} @parts)), "\n";
425             warn "\n\n\n";
426             }
427              
428             return(@parts);
429             } # _part_slide ########################################################
430              
431             =head2 _calc_width
432              
433             my $n = $self->_calc_width($text);
434              
435             =cut
436              
437             sub _calc_width {
438             my $self = shift;
439             my $text = shift;
440              
441             my @lines = split(/\n|/, $text);
442             my ($width) = sort({$b <=> $a}
443             map({s/<[^>]+>//g; s/&[^;]+;/./g; length($_)} @lines));
444             $width *= 0.625; # emperical em-width adjustment
445             } # _calc_width ########################################################
446              
447             =head2 _handle_parts
448              
449             $self->_handle_parts($ctx, \@parts, %opts);
450              
451             =cut
452              
453             sub _handle_parts {
454             my $self = shift;
455             my ($ctx, $parts, %opts) = @_;
456              
457             my @parts = @$parts;
458             my $calc_width = $opts{calc_width};
459              
460             if($parts[2] and @{$parts[1]} == 0) {
461             my $title_chunk = shift(@parts);
462             shift(@parts); # scrap
463             $ctx->create_child(div => [class => 'title'], @$title_chunk);
464             $ctx->create_child('br');
465             }
466             else {
467             # center the whole thing vertically
468             $ctx = $ctx->create_child(div => [class => 'cell']);
469             }
470              
471             while(@parts) {
472             my $part = shift(@parts);
473             next unless(@$part);
474             if(@$part == 1 and $part->[0] =~ m/^[^<]*[^<]*$/) {
475             $ctx->add_child($part->[0]);
476             next;
477             }
478             # pre fixup
479             if(@$part == 1 and $part->[0]->tag eq 'pre') {
480             my ($pre) = @$part;
481             my $text = join('', $pre->children);
482             $text =~ s/^\n//;
483             if($text =~ s/^(\s+)//) {
484             my $sp = $1;
485             $text =~ s/^$sp//mg;
486             }
487             my %atts = $pre->atts;
488             my $class = $atts{class} || '';
489             $pre->{children} = [];
490             $pre->create_child('' => $text);
491             my $width = $calc_width->($text);
492             my $inner = $ctx->create_child(
493             div => [class => "auto left $class",
494             style => "width: $width"]);
495             $inner->add_child($pre);
496             next;
497             }
498             # bullet points
499             if($part->[0] =~ m/^(\s*)\* /) {
500             my $sp = $1;
501             my @points = $part;
502             # then go back to the well:
503             while(@parts and $parts[0][0] =~ m/^\s*\* /) {
504             push(@points, shift(@parts));
505             }
506             foreach my $point (@points) {
507             $point->[0]->is_text or die;
508             $point->[0]->{content} =~ s/^$sp//;
509             }
510              
511             my $width = $calc_width->(join("\n", map({@$_} @points)));
512             my $inner = $ctx->create_child(
513             div => [class => "auto left", style => "width: $width"]);
514             my $top = $inner->create_child(ul =>);
515             my @d = ($top);
516             foreach my $point (@points) {
517             $point->[0]->{content} =~ s/(\s*)\*\s+//;
518             my $ws = length($1)/2;
519             # warn "ws: $ws ($point->[0]->{content})\n";
520             if($ws) {
521             $d[$ws] ||= $d[$ws-1]->child(-1)->create_child(ul =>);
522             }
523             else {
524             @d = ($top);
525             }
526             $d[$ws]->create_child(li => @$point);
527             }
528             # warn "yay: $top\n";
529             next;
530             }
531             my $inner = $ctx->create_child(div =>);
532             $inner->add_child($_) for(@$part);
533             }
534              
535             } # _handle_parts ######################################################
536              
537             =head2 _mk_script
538              
539             $self->_mk_script(%opts);
540              
541             =cut
542              
543             sub _mk_script {
544             my $self = shift;
545             my (%opts) = @_;
546              
547             my $script =
548             ($opts{next} ? qq(var next="$opts{next}"\n) .
549             "var down=0; document.onmousedown=function(e) { down=1 }\n".
550             " document.onmousemove=function(e) { down=0; }\n".
551             "document.onmouseup=function(e) {\n" .
552             "if(down == 1) {window.location = next;}; }\n" : ''
553             ) .
554             ($opts{prev} ? qq(var prev="$opts{prev}"\n) : '') .
555             ($opts{first} ? qq(var first="$opts{first}"\n) : '') .
556             ($opts{last} ? qq(var last="$opts{last}"\n) : '');
557             my $func = <<' ---';
558             document.onkeypress=function(e) {
559             var e=window.event || e
560             var n=e.keyCode || e.which
561             switch (n) {
562             -SWITCH-
563             }
564             }
565             ---
566             my $switch = join("\n", map({$_ . ' break;'}
567             ($opts{next} ? 'case 32 : window.location = next;' : ()),
568             ($opts{prev} ? 'case 8 : window.location = prev;' : ()),
569             ($opts{first} ? 'case 36 : window.location = first;' : ()),
570             ($opts{last} ? 'case 35 : window.location = last;' : ()),
571             ));
572             $func =~ s/-SWITCH-/$switch/;
573              
574             return($script . $func);
575             } # _mk_script #########################################################
576              
577             =head1 AUTHOR
578              
579             Eric Wilhelm @
580              
581             http://scratchcomputing.com/
582              
583             =head1 BUGS
584              
585             If you found this module on CPAN, please report any bugs or feature
586             requests through the web interface at L. I will be
587             notified, and then you'll automatically be notified of progress on your
588             bug as I make changes.
589              
590             If you pulled this development version from my /svn/, please contact me
591             directly.
592              
593             =head1 COPYRIGHT
594              
595             Copyright (C) 2009 Eric L. Wilhelm, All Rights Reserved.
596              
597             =head1 NO WARRANTY
598              
599             Absolutely, positively NO WARRANTY, neither express or implied, is
600             offered with this software. You use this software at your own risk. In
601             case of loss, no person or entity owes you anything whatsoever. You
602             have been warned.
603              
604             =head1 LICENSE
605              
606             This program is free software; you can redistribute it and/or modify it
607             under the same terms as Perl itself.
608              
609             =cut
610              
611             # vi:ts=2:sw=2:et:sta
612             1;