File Coverage

blib/lib/Shebangml.pm
Criterion Covered Total %
statement 27 259 10.4
branch 0 118 0.0
condition 0 17 0.0
subroutine 9 30 30.0
pod 16 16 100.0
total 52 440 11.8


line stmt bran cond sub pod time code
1             package Shebangml;
2             $VERSION = v0.0.1;
3              
4 1     1   5142 use warnings;
  1         2  
  1         44  
5 1     1   5 use strict;
  1         2  
  1         31  
6 1     1   19 use Carp;
  1         1  
  1         85  
7              
8             =head1 NAME
9              
10             Shebangml - markup with bacon
11              
12             =head1 SYNOPSIS
13              
14             This is an experimental markup language + parser|interpreter with
15             support for plugins and cleanly configurable add-on features. I use it
16             as a personal home page tool and lots of other things.
17              
18             See L for details.
19              
20             =cut
21              
22 1     1   4518 use Class::Accessor::Classy;
  1         12452  
  1         17  
23             with 'new';
24             ro 'state';
25             rw 'out_fh';
26 1     1   259 no Class::Accessor::Classy;
  1         2  
  1         7  
27              
28 1     1   308 use constant DEBUG => 0;
  1         1  
  1         91  
29              
30             # XXX experimental global variable and accessor :-/
31 0     0 1   our $current_file; sub current_file {$current_file};
32              
33 1     1   756 use Shebangml::State;
  1         3  
  1         4958  
34              
35             =head1 Methods
36              
37             =head2 configure
38              
39             $hbml->configure(%options);
40              
41             =cut
42              
43             sub configure {
44 0     0 1   my $self = shift;
45 0           my (%opts) = @_;
46              
47 0 0         if(my $h = $opts{handlers}) {
48 0           while(my ($name, $pm) = each(%$h)) {
49 0           require($pm);
50 0           $self->add_handler($name);
51             }
52             }
53             } # end subroutine configure definition
54             ########################################################################
55              
56             =head2 add_handler
57              
58             Adds a handler for a namespace.
59              
60             $hbml->add_handler($name);
61              
62             The C<$name> will have C prepended to it, and
63             should already be loaded at this point. It is good practice to declare
64             a version (e.g. C) in your handler package -- and
65             may be required in the future.
66              
67             If a C method is available, a new object will be constructed and
68             stored as the handler. Otherwise, the handler will be treated as a
69             class name. Tags in the handlers namespace are constructed as:
70              
71             .yourclass.themethod[foo=bar]
72              
73             or
74              
75             .yourclass.themethod[foo=bar]{{{content literal}}}
76              
77             These would cause the processing to invoke one of the following (the
78             latter if you have defined C) and send the result to
79             C<$hbml-Eoutput()>.
80              
81             Shebangml::Handler::yourclass->themethod($atts, $content);
82              
83             $yourobject->themethod($atts, $content);
84              
85             =cut
86              
87             sub add_handler {
88 0     0 1   my $self = shift;
89 0           my ($name, $what) = @_;
90              
91 0 0         if($what) {
92 0           die "teach me that trick please";
93             }
94             else {
95 0           $what = 'Shebangml::Handler::' . $name;
96 0 0         if(my $construct = $what->can('new')) {
97 0           $what = $what->$construct;
98             }
99             }
100              
101 0   0       my $h = $self->{handlers} ||= {};
102 0           $h->{$name} = $what;
103             } # end subroutine add_handler definition
104             ########################################################################
105              
106             =head2 add_hook
107              
108             $hbml->add_hook($name => sub {...});
109              
110             =cut
111              
112             sub add_hook {
113 0     0 1   my $self = shift;
114 0           my ($what, $hook) = @_;
115              
116 0           $self->{hooks}{$what} = $hook;
117             } # end subroutine add_hook definition
118             ########################################################################
119              
120             =head2 process
121              
122             Processes a given input $source. This method holds its own state and
123             can be repeatedly called with new inputs (each of which must be a
124             well-formed shebangml document) using the same $hbml object.
125              
126             Arguments are passed to L.
127              
128             $hbml->process($source);
129              
130             =cut
131              
132             sub process {
133 0     0 1   my $self = shift;
134 0           my $state = Shebangml::State->new(@_);
135 0           local $current_file = $current_file;
136 0   0       $current_file ||= $state->{filename} || undef;
      0        
137              
138 0           my @opened;
139 0           my $bare = 0;
140 0           my $in_att = 0;
141 0           while(my $CL = $state->next) {
142              
143             # absorb the comments
144 0 0         if($$CL =~ m/^\s*#/) {
145 0           $state->skip_comment;
146 0           next;
147             }
148              
149             # main processing of the current line
150 0           while($$CL =~ s/^(.*?)([\.\w-]+[\{\[]|\]\{|[\[\]\{\}]|\n)//x) {
151 0           my ($text, $hit) = ($1, $2);
152 0           DEBUG and warn join(',', $text, $hit), "\n";
153 0 0         if($hit) {
154 0           my $escaped;
155 0 0         if($text =~ s/(\\+)$//) {
156 0           my $bs = $1;
157 0           my $n = length($bs);
158             # TODO put-back half of them
159 0 0         if($n %2) {
160 0           $escaped = 1;
161 0           chop($bs);
162             }
163 0           $text .= $bs;
164             }
165 0 0 0       if($hit eq '{') {
    0          
    0          
    0          
    0          
166             # so what? Should I count them?
167 0           DEBUG and warn "# Bare {\n";
168 0 0         $bare++ unless($escaped);
169 0           $text .= $hit;
170             }
171             elsif($hit eq '[') {
172 0           $text .= $hit;
173             }
174             elsif($hit eq '}') {
175 0 0         if($escaped) {
    0          
176 0           $text .= $hit;
177             }
178             elsif($bare) {
179 0           $bare--;
180 0           $text .= $hit;
181             }
182             else { # closing
183 0 0         my $guts = pop(@opened) or
184             croak("no open tag where closing ($text)");
185 0           $self->put_text($text); $text = '';
  0            
186 0           my $tag = $guts->[0];
187 0           $self->put_tag_end($tag);
188 0 0         if($$CL =~ s/#([\.\w]+);//) {
189 0 0         $1 eq $tag or croak("assertion $tag failed: $1");
190             }
191             }
192             }
193             elsif($hit eq ']' or $hit eq "]\{") {
194 0 0         if($in_att) { # everything in $text is attributes now
195 0           my @guts = @{$opened[-1]};
  0            
196 0           $text =~ s/^\s*//;
197 0           my $tag = shift(@guts);
198              
199 0   0       my $atts = $self->atts(@guts, $text||());
200              
201             # put_tag_start with attributes
202 0 0         if($hit eq "]\{") {
203             # look for fat quote
204 0 0         if($$CL =~ s/^\{\{(\n?)//) {
205 0           my $cr = $1;
206 0           pop(@opened);
207 0           DEBUG and warn "thick bacon!\n";
208 0           $self->put_tag($tag, $atts, $state->read_literal($tag, $cr));
209             }
210             else {
211 0           $self->put_tag_start($tag, $atts);
212             }
213             }
214             else {
215 0           $self->put_tag($tag, $atts);
216 0           pop(@opened);
217             }
218 0           $text = '';
219              
220 0           $in_att = 0;
221             }
222             else { # no need to escape these brackets
223             # XXX that's probably incorrect for the \]\{ case
224 0           $text .= $hit;
225             }
226             }
227             elsif($hit eq "\n") {
228 0 0         if($in_att) {
229 0           push(@{$opened[-1]}, $text);
  0            
230 0           $text = '';
231             }
232             else {
233 0 0         if($escaped) {
234             # we dropped the $bs earlier so munch whitespace ...
235 0           $state->skip_whitespace;
236             }
237             else {
238 0           $text .= $hit;
239             }
240             }
241             }
242             else {
243 0 0         my ($tag, $br) = ($hit =~ m/^(.*)([\[\{])/) or die "ouch";
244 0           DEBUG and warn "yay: $tag --> $br\n";
245 0           my $guts = [$tag];
246 0           push(@opened, $guts);
247              
248 0           $self->put_text($text); $text = '';
  0            
249              
250 0 0         if($br eq '[') { # TODO greedy attribute grab?
251 0           $in_att = 1;
252             # TODO $self->put_tag_start goes here if we gobble the atts
253             # (But then I also have to deal with the fatquote)
254             }
255             else {
256 0 0         if($$CL =~ s/^\{\{(\n?)//) {
257 0           my $cr = $1;
258 0           pop(@opened);
259 0           DEBUG and warn "thick bacon\n";
260 0           $self->put_tag($tag, undef, $state->read_literal($tag, $cr));
261             }
262             else {
263             # if we have text here, it preceded the tag
264 0           $self->put_tag_start($tag);
265             }
266             }
267             }
268             #die "text! $text" if($text);
269             }
270             else { # no hit
271             # TODO text-only output only here?
272             }
273              
274             # XXX we shouldn't have anything to output here after refactoring
275             # warn "output ($text)\n";
276             # die "argh ($text)" if($text ne "\n");
277 0           $self->put_text($text);
278              
279             # more whitespace munching
280 0 0         if($$CL =~ s/^\\\s+//) {
281 0 0         $state->skip_whitespace if($$CL eq '');
282             }
283              
284             } # end $CL muncher
285             }
286              
287             } # end subroutine process definition
288             ########################################################################
289              
290             =head2 put_tag
291              
292             Handles contentless tags and any tags constructed with the {{{ ... }}}
293             literal quoting mechanism.
294              
295             $hbml->put_tag($tag, $atts, $string);
296              
297             =cut
298              
299             sub put_tag {
300 0     0 1   my $self = shift;
301 0           my ($tag, $atts, $string) = @_;
302              
303 0 0         if($tag =~ s/^\.//) { return $self->run_tag($tag, $atts, $string) }
  0            
304              
305 0 0         if(my $hook = $self->{hooks}{$tag}) {
306 0           $hook->($tag, $atts);
307             }
308              
309 0 0         if(defined($string)) {
310 0           $self->put_tag_start($tag, $atts);
311 0           $self->put_literal($string);
312 0           $self->put_tag_end($tag);
313             }
314             else {
315 0 0         $self->output('<' . $tag . ($atts ? $atts->as_string : '') . ' />');
316             }
317             } # end subroutine put_tag definition
318             ########################################################################
319              
320             =head2 put_tag_start
321              
322             $hbml->put_tag_start($tag, $atts);
323              
324             =cut
325              
326             sub put_tag_start {
327 0     0 1   my $self = shift;
328 0           my ($tag, $atts) = @_;
329              
330 0 0         if($tag =~ s/^\.//) { return $self->run_tag($tag, $atts) }
  0            
331              
332 0 0         if(my $hook = $self->{hooks}{$tag}) {
333 0           $hook->($tag, $atts);
334             }
335              
336 0 0         $self->output('<' . $tag . ($atts ? $atts->as_string : '') . '>');
337             } # end subroutine put_tag_start definition
338             ########################################################################
339              
340             =head2 put_tag_end
341              
342             $hbml->put_tag_end($tag);
343              
344             =cut
345              
346             sub put_tag_end {
347 0     0 1   my $self = shift;
348 0           my ($tag) = @_;
349              
350 0 0         if($tag =~ s/^\.//) { return $self->run_tag($tag) }
  0            
351              
352 0           $self->output('');
353             } # end subroutine put_tag_end definition
354             ########################################################################
355              
356             =head2 put_text
357              
358             $hbml->put_text($text);
359              
360             =cut
361              
362             sub put_text {
363 0     0 1   my $self = shift;
364 0           my ($text) = @_;
365 0 0         $text or return; # XXX still need to signal?
366              
367 0           $self->output($self->escape_text($text));
368             } # end subroutine put_text definition
369             ########################################################################
370              
371              
372             =head2 run_tag
373              
374             This method is called for any whole, starting, or ending tags which
375             start with a dot ('.'). The builtin or plugin handler for the given tag
376             I exist and I have a prototype which corresponds to the way
377             it is used.
378              
379             $hbml->run_tag($tag, @and_stuff);
380              
381             Yes, your method should have a prototype.
382              
383             =cut
384              
385             sub run_tag {
386 0     0 1   my $self = shift;
387 0           my ($tag, @and) = @_;
388              
389             my $call = sub {
390 0     0     my ($h, $m) = @_;
391 0           my $proto = prototype($m);
392 0 0         croak("$tag prototype not defined") unless(defined $proto);
393 0 0         croak("$tag prototype ($proto) invalid") unless($proto =~ m/^;?\$\$?$/);
394              
395 0 0         unless(@and) {
396 0 0         $proto =~ m/^;/ or
397             croak("$tag prototype ($proto) disallows start/end usage");
398             }
399              
400 0           return($h->$m(@and));
401 0           };
402              
403 0 0         if($tag =~ s/^x\.//) {
404 0           my ($name, $method, @more) = split(/\./, $tag);
405 0 0         my $handler = $self->{handlers}{$name} or
406             croak("no handler for $name");
407 0 0         my $ref = $handler->can($method) or
408             croak("cannot find $method in $handler");
409 0           while(@more) {
410 0           $handler = $handler->$ref;
411 0           $method = shift(@more);
412 0 0         $ref = $handler->can($method) or
413             croak("cannot find $method in $handler");
414             }
415 0           $method = $ref;
416 0           return $self->output($call->($handler, $method));
417             }
418             else {
419 0 0         my $method = $self->can('do_' . $tag) or
420             croak("no builtin for .$tag");
421 0           return $call->($self, $method);
422             }
423             } # run_tag ############################################################
424              
425             =head2 escape_text
426              
427             my $out = $hbml->escape_text($text);
428              
429             =cut
430              
431             sub escape_text {
432 0     0 1   my $self = shift;
433 0           my ($text) = @_;
434              
435             # escaping '&','<' and everything else
436 0           $text =~ s/&/&/g;
437 0           $text =~ s/
438             # must break-out all of the double backslashes I guess
439 0           my @parts = split(/\\\\/, $text);
440 0           for(@parts) {
441 0           s#\\n;#
#g;
442 0           s/\\#(\d+|x[0-9a-f]+);/&#$1;/gi;
443 0           s/\\#/#/g;
444 0           s/\\_;/ /g; # XXX that should be utf8 nbsp?
445 0           s/\\-;/–/g;
446 0           s/\\--;/—/g;
447 0           s#\\---;#
#g;
448 0           s/\\(\w+);/&$1;/g;
449             }
450              
451 0           return(join('\\', @parts));
452             } # escape_text ########################################################
453              
454             =head2 put_literal
455              
456             $hbml->put_literal($string);
457              
458             =cut
459              
460             sub put_literal {
461 0     0 1   my $self = shift;
462 0           my ($string) = @_;
463              
464             # TODO trigger text hooks
465 0           $self->output($string);
466             } # end subroutine put_literal definition
467             ########################################################################
468              
469             =head2 output
470              
471             $hbml->output(@strings);
472              
473             =cut
474              
475             sub output {
476 0     0 1   my $self = shift;
477 0           my (@strings) = @_;
478              
479 0 0         my $out_fh = $self->out_fh or croak("no output fh");
480 0           print $out_fh @strings;
481             } # end subroutine output definition
482             ########################################################################
483              
484             =head1 Builtins
485              
486             =head2 do_include
487              
488             $hbml->do_include($atts);
489              
490             =cut
491              
492             sub do_include ($$) {
493 0     0 1   my $self = shift;
494 0           my ($atts) = @_;
495 0 0         my $filename = $atts->get('src') or croak("need filename for include");
496 0           $self->process($filename);
497             } # end subroutine do_include definition
498             ########################################################################
499              
500             =head2 do_doctype
501              
502             $hbml->do_doctype($atts);
503              
504             =cut
505              
506             sub do_doctype ($$) {
507 0     0 1   my $self = shift;
508 0 0         (@_ == 2) or croak('.doctype cannot have data');
509 0           my ($atts) = @_;
510 0 0         my $opt = $atts->get('id') or croak("must select doctype with =type");
511              
512 0           my %types = (
513             html_strict =>
514             q(
515             q( "http://www.w3.org/TR/html4/strict.dtd">),
516              
517             html_loose =>
518             q(
519             q( "http://www.w3.org/TR/html4/loose.dtd">),
520              
521             html_frameset =>
522             q(
523             q( "http://www.w3.org/TR/html4/frameset.dtd">),
524              
525             x_strict =>
526             q(
527             q( "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">),
528              
529             x_loose =>
530             q(
531             q( "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">),
532              
533             x_frameset =>
534             q(
535             q( "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">),
536              
537             xhtml11 =>
538             q(
539             q( "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">),
540             );
541 0 0         my $string = $types{$opt} or
542             croak("$opt is not one of ", join(", ", sort(keys %types)));
543              
544 0           $self->output($string);
545             } # end subroutine do_doctype definition
546             ########################################################################
547              
548             {
549             package Shebangml::Attrs;
550 1     1   13 use Class::Accessor::Classy;
  1         3  
  1         15  
551             with 'new';
552             lw 'atts';
553             #ri 'as_string'; # ugh
554 1     1   244 no Class::Accessor::Classy;
  1         3  
  1         5  
555              
556              
557             =for head2 as_string
558             Output pairs with = and quoting, leading space and spaces between them.
559             $atts->as_string;
560              
561             =cut
562              
563             sub as_string {
564 0     0     my $self = shift;
565              
566             # quote and = the pairs
567 0           my @atts = $self->atts;
568 0 0         croak(scalar(@atts), ' items cannot be a list of pairs')
569             if(@atts % 2);
570              
571 0           return(' ' . join(' ',
572 0           map({$atts[2*$_] . '="' . $atts[2*$_+1] . '"'} 0..(($#atts-1)/2))
573             ));
574             } # end subroutine as_string definition
575             ########################################################################
576              
577             =for head2 get
578             $atts->get($name);
579              
580             =cut
581              
582             sub get {
583 0     0     my $self = shift;
584 0           my ($name) = @_;
585              
586 0           my @atts = $self->atts;
587 0           my @ans = map({$atts[2*$_+1]}
  0            
588 0           grep({$atts[$_*2] eq $name} 0..(($#atts-1)/2)));
589 0 0         @ans or return();
590 0 0         return(@ans == 1 ? ($ans[0]) : @ans);
591             } # end subroutine get definition
592             ########################################################################
593              
594             =for head2 delete
595             my $v = $atts->delete($name);
596              
597             =cut
598              
599             sub delete {
600 0     0     my $self = shift;
601 0           my ($name) = @_;
602              
603 0   0       my $atts = $self->{atts} ||= [];
604 0           for(my $i = 0; $i < @$atts; $i+=2) {
605 0 0         if($atts->[$i] eq $name) {
606 0           return scalar splice(@$atts, $i, 2);
607             }
608             }
609 0           return();
610             } # delete #############################################################
611              
612             =for head2 set
613             $atts->set($name => $value);
614              
615             =cut
616              
617             sub set {
618 0     0     my $self = shift;
619 0           my ($n, $v) = @_;
620 0   0       my $atts = $self->{atts} ||= [];
621 0           for(my $i = 0; $i < @$atts; $i+=2) {
622 0 0         if($atts->[$i] eq $n) {
623 0           return $atts->[$i+1] = $v;
624             }
625             }
626 0           push(@$atts, $n, $v);
627 0           return($v);
628             } # set ################################################################
629              
630             1;
631             }
632              
633             =head2 atts
634              
635             Parses one or more lines of attribute strings into pairs and returns an
636             atts object.
637              
638             my $atts = $self->atts(@atts);
639              
640             =cut
641              
642             # XXX guess this needs to return an object with accessors and a string
643             # method to preserve the original linebreaks and junk.
644             sub atts {
645 0     0 1   my $self = shift;
646 0           my (@atts) = @_;
647              
648 0 0         @atts or return();
649 0           s/\n/ /g for(@atts);
650 0           my $input = join(' ', @atts);
651              
652             # leading whitespace, multiline attributes, etc
653             # UGH. I think I would rather just collapse them
654             # /=(\w)/="$1/ and /(\w) /$1"/ <-- but not when quoted
655             # join it all together?
656             # just split and then sort it out?
657              
658 0           my $attr = Shebangml::Attrs->new(atts => []);
659              
660             # shortcuts for id=, name=, class=
661 0           my %short = (qw(
662             : name
663             = id
664             @ class
665             ));
666 0           my $sigil = '[' . join('', keys %short) . ']';
667 0           my $bareword = qr/[\/:._\w-]+/;
668 0           my %did = map({$_ => 0} keys %short);
  0            
669 0           while($input =~ s/^(\s*)($sigil)($bareword)//) {
670 0           my ($ws, $f, $v) = ($1, $2, $3);
671 0 0         my $n = $short{$f} or croak("no shortcut $f");
672 0 0         $did{$f}++ and croak("duplicate shortcut $n");
673 0           $attr->add_atts($n, $v);
674             }
675              
676             # the rest is straight xml, but only optionally quoted
677 0           while($input =~ m/\G(\s*)
678             ($bareword) = ("(?:\\.|[^"])*" | $bareword)
679             (\s*)/gx) {
680 0           my ($lws, $name, $val, $tws) = ($1, $2, $3, $4);
681 0           $val =~ s/^"//; $val =~ s/"$//;
  0            
682 0           $attr->add_atts($name, $val);
683             }
684              
685 0           return($attr);
686             } # end subroutine atts definition
687             ########################################################################
688              
689             =head1 Experimental
690              
691             Some parts which might not survive revision:
692              
693             =head2 current_file
694              
695             This is set during process() and becomes accessible for callbacks as a
696             class accessor.
697              
698             =cut
699              
700             =head1 AUTHOR
701              
702             Eric Wilhelm @
703              
704             http://scratchcomputing.com/
705              
706             =head1 BUGS
707              
708             If you found this module on CPAN, please report any bugs or feature
709             requests through the web interface at L. I will be
710             notified, and then you'll automatically be notified of progress on your
711             bug as I make changes.
712              
713             If you pulled this development version from my /svn/, please contact me
714             directly.
715              
716             =head1 COPYRIGHT
717              
718             Copyright (C) 2008 Eric L. Wilhelm, All Rights Reserved.
719              
720             =head1 NO WARRANTY
721              
722             Absolutely, positively NO WARRANTY, neither express or implied, is
723             offered with this software. You use this software at your own risk. In
724             case of loss, no person or entity owes you anything whatsoever. You
725             have been warned.
726              
727             =head1 LICENSE
728              
729             This program is free software; you can redistribute it and/or modify it
730             under the same terms as Perl itself.
731              
732             =cut
733              
734             # vi:ts=2:sw=2:et:sta
735             1;