File Coverage

blib/lib/Text/Nimble.pm
Criterion Covered Total %
statement 414 424 97.6
branch 252 280 90.0
condition 44 72 61.1
subroutine 16 16 100.0
pod 2 2 100.0
total 728 794 91.6


line stmt bran cond sub pod time code
1             package Text::Nimble;
2              
3 2     2   42037 use 5.010; # for recursive regexes, but also using //
  2         8  
  2         87  
4 2     2   13 use strict;
  2         3  
  2         76  
5 2     2   11 use warnings FATAL => 'all';
  2         9  
  2         105  
6              
7 2     2   12 use Carp;
  2         3  
  2         15714  
8              
9             our $VERSION = '0.002001';
10             $VERSION = eval $VERSION;
11              
12              
13             my %renderer = (
14             html => \&_renderer_html,
15             );
16              
17              
18             sub render {
19 4     4 1 11060 my ($format, $nimble) = @_;
20 4 50       14 croak "Text::Nimble::render takes two arguments but got ".(scalar @_) unless @_ == 2;
21 4 50 33     39 croak "Text::Nimble::render takes a render format name as its first argument" unless defined $_[0] && $_[0] =~ /^\w+$/;
22 4 50 66     53 croak "Text::Nimble::render takes Nimble text or a Nimble parse result as its second argument" unless defined $_[1] && (!ref $_[1] || ref $_[1] eq 'ARRAY' || (ref $_[1] eq 'HASH' && $_[1]{tree} && $_[1]{macro}));
      33        
23 4 50       16 croak "Text::Nimble::render was given an unknown render format '$_[0]'" unless $renderer{$format};
24              
25 4 100       17 $nimble = parse($nimble) unless ref $nimble eq 'HASH';
26 4         18 my $output = $renderer{$format}($nimble);
27 4 100       732 return wantarray ? ($output, $nimble->{meta}, $nimble->{error}) : $output;
28             }
29              
30             sub parse {
31 5 50   5 1 205113 croak "Text::Nimble::parse takes one argument but got ".(scalar @_) unless @_ == 1;
32              
33 5         8 my (%meta, %macro, @error);
34              
35             local *_nimble_makelines = sub {
36             # break input into lines
37 5 50 33 5   66 croak "Text::Nimble::parse expects a string" unless @_==1 && defined $_[0] && !ref $_[0];
      33        
38 5         11 my $input = $_[0];
39 5         70 $input =~ s/\r//g;
40 5         669 my @lines = split(/\n/, $input, -1);
41              
42 5         71 my $next_indent;
43 5         23 for (my $i=$#lines; $i>=0; $i--) {
44 2215 100 33     6857 if ($lines[$i] =~ /^( *)\S/) {
    50          
45 1550         3714 $next_indent = $1;
46             } elsif (defined $next_indent && $lines[$i] =~ /^\s*$/) {
47 665         1925 $lines[$i] = $next_indent;
48             }
49             }
50              
51 5         16 return \@lines;
52 5         32 };
53              
54             local *_macro_arg_preprocess = sub {
55 170     170   249 my ($type, $arg) = @_;
56              
57 170 100       383 if ($type eq 'nimble') {
    50          
58 30 100       87 return ref $arg ? _nimble_parse($arg) : _nimble_parse_inline($arg);
59             } elsif ($type eq 'raw') {
60 140 100       624 return ref $arg ? join("\n", @$arg) : $arg;
61             } else {
62 0         0 die "unreachable: _macro_arg_preprocess tried to decode a macro argument with unrecognized type '$type'";
63             }
64 5         26 };
65              
66             local *_macro_invocation_preprocess = sub {
67 50     50   81 my ($macro_name, $node) = @_;
68 50         76 foreach my $arg (keys %{$macro{$macro_name}{args}}) {
  50         151  
69 105 100       396 $node->{args}{$arg} =
70             defined $node->{args}{$arg}
71             ? _macro_arg_preprocess($macro{$macro_name}{args}{$arg}{type}, $node->{args}{$arg})
72             : $macro{$macro_name}{args}{$arg}{default};
73             }
74 5         24 };
75              
76 5         23 my %inlinestyle = (
77             '*' => 'strong',
78             '/' => 'emphasis',
79             );
80 5         42 my $inlinestyle_re = '['.quotemeta(join('', keys %inlinestyle)).']';
81              
82 5         40 my %entityshorthand = (
83             '--' => '–',
84             '---' => '—',
85             '<--' => '←',
86             '-->' => '→',
87             '<==' => '⇐',
88             '==>' => '⇒',
89             );
90 5         35 my $entityshorthand_re = '(?:'.join('|', map {quotemeta} sort {length $b <=> length $a} keys %entityshorthand).')';
  30         56  
  49         71  
91              
92 5         13152 my $inline_re = qr/
93             # start at pos(), then group for delimiter-matching recursion
94             # 1
95             \G(
96             # plain text or escapes
97             # 2
98             ((?:[\w\s\.\,\(\)\']+|\\.)+)
99             # inline code
100             # 3 4
101             | (\`+)\s*(.*?)\s*\g{-2}
102             # inline style
103             # 5 6
104             | ($inlinestyle_re)(?!\s)((?>(?1))+?)(?
105             # entity literal
106             # 7
107             | (\&(?:\#(?:\d+|x[0-9a-fA-F]+)|\w+)\;)
108             # entity shorthand
109             # 8
110             | ($entityshorthand_re)
111             # brackets
112             # 9
113             | \[((?>(?1))+?)\]
114             # failure; take any one character
115             # 10
116             | (.)
117             )
118             /x;
119              
120             local *_nimble_parse_inline = sub {
121 1155     1155   1603 my $text = shift;
122 1155 50 33     6878 croak "_nimble_parse_inline takes exactly one nonref defined argument" if @_ || !defined $text || ref $text;
      33        
123              
124 1155         1142 my @output;
125             my $append_text = sub {
126 2330 100 100     8109 if (@output && $output[-1]{type} eq 'text') {
127 835         7947 $output[-1]{text} .= $_[0];
128             } else {
129 1495         13454 push @output, {type=>"text", text=>$_[0]};
130             }
131 1155         3749 };
132 1155         8087 while ($text =~ /$inline_re/g) {
133 2825         8731 my ($plain, $code, $styletype, $stylecontent, $entity_literal, $entity_shorthand, $bracketcontents, $char)
134             = ($2, $4, $5, $6, $7, $8, $9, $10 );
135              
136 2825 100       7283 if (defined $plain) {
    100          
    100          
    100          
    100          
    100          
    50          
137 1795         2557 $plain =~ s/\\(.)/$1/g;
138 1795         3150 $append_text->($plain);
139             } elsif (defined $code) {
140 70         542 push @output, {type=>"code", text=>$code};
141             } elsif (defined $styletype) {
142 210         555 push @output, {type=>$inlinestyle{$styletype}, content=>_nimble_parse_inline($stylecontent)};
143             } elsif (defined $entity_literal) {
144 20         143 push @output, {type=>"entity", html=>$entity_literal};
145             } elsif (defined $entity_shorthand) {
146 70         541 push @output, {type=>"entity", html=>$entityshorthand{$entity_shorthand}};
147             } elsif (defined $bracketcontents) {
148 145 100       1241 if ($bracketcontents =~ /^\$(\w+)((?:\s+\w+\=(?:\S*|([\"\'])((?:(?!\\|\g{-2}).|\\.)*)\g{-2}))*)\s*$/) {
    100          
    100          
    100          
149 65         161 my ($macro_name, $macro_args) = ($1, $2);
150 65         74 my ($node, @errors);
151              
152 65 100       196 push @errors, "no macro by that name is defined" unless $macro{$macro_name};
153              
154 65 100       149 if (!@errors) {
155 60         199 $node = {type=>"macro", macro=>$macro_name, args=>{}};
156 60         347 while ($macro_args =~ /\G\s+(\w+)\=(?:(?![\"\'])(\S*)|([\"\'])((?:(?!\\|\g{-2}).|\\.)*)\g{-2})/g) {
157 150   66     527 my ($arg_name, $arg_value) = ($1, $2 // $4);
158 150 100       395 push @errors, "duplicate argument '$arg_name'" if $node->{args}{$arg_name};
159 150 100       439 push @errors, "unknown argument '$arg_name'" unless $macro{$macro_name}{args}{$arg_name};
160 150 100       428 next if @errors; #skip work if any errors have been seen, but keep parsing to find more errors
161 95 50       215 $arg_value =~ s/^\[|\]$//g if $arg_value =~ /^(\[(?:(?:(?-1)*?)|[^\[\]\\]+|\\.|.)*?\])$/;
162 95         150 $arg_value =~ s/\\(.)/$1/g;
163 95         575 $node->{args}{$arg_name} = $arg_value;
164             }
165 60 100       187 if (!@errors) {
166 35         81 _macro_invocation_preprocess($macro_name, $node);
167             }
168             }
169              
170 65 100       173 if (@errors) {
171 30         116 $node = {type=>"error", context=>"building inline macro '$macro_name' invocation", errors=>\@errors};
172 30         86 push @error, $node;
173             }
174 65         404 push @output, $node;
175             } elsif ($bracketcontents =~ /^raw\s+(\w+)\s+(.+?)\s*$/) {
176 5         18 my ($format, $content) = ($1, $2);
177 5         14 $content =~ s/\\(.)/$1/g;
178 5         52 push @output, {type=>"raw", format=>$format, content=>$content};
179             } elsif ($bracketcontents =~ /^img\s+(\S+)(?:\s+(.+?))?$/) {
180 25         46 my $url = $1;
181 25   50     86 my $alt = $2 // "";
182 25         39 $url =~ s/\\(.)/$1/g;
183 25         86 $alt =~ s/\\(.)/$1/g;
184 25         220 push @output, {type=>"img", src=>$url, alt=>$alt};
185             } elsif ($bracketcontents =~ /^(?:link\s+)?(\S+)(?:\s+(.+?))?$/) {
186 40         74 my $url = $1;
187 40         66 my $content = $2;
188 40         103 $url =~ s/\\(.)/$1/g;
189 40 100       202 push @output, {type=>"link", url=>$url, content=>(defined $content ? _nimble_parse_inline($content) : [{type=>"text", text=>$url}])};
190             } else {
191 10         18 $append_text->('[');
192 10         14 push @output, @{_nimble_parse_inline($bracketcontents)};
  10         21  
193 10         21 $append_text->(']');
194             }
195             } elsif (defined $char) {
196 515         819 $append_text->($char);
197             } else {
198 0         0 die "unreachable: _nimble_parse_inline matched an inline pattern but failed to determine which rule should handle it: " . substr($text, $-[0], $+[0] - $-[0]) . "\n";
199             }
200             }
201              
202 1155         7437 return \@output;
203 5         1018 };
204              
205             local *_nimble_parse = sub {
206 595 50 33 595   2497 croak "_nimble_parse takes 1-2 arguments but got ".(scalar @_) if @_ < 1 || @_ > 2;
207 595 50 66     1427 croak "_nimble_parse expects an arrayref for its second argument" if @_==2 && !ref($_[1]) eq 'ARRAY';
208              
209 595         610 my @lines = @{$_[0]};
  595         1524  
210              
211 595 100       1134 my @extra_rules = $_[1] ? @{$_[1]} : ();
  35         62  
212              
213 595         1093 my $section_depth = 0;
214              
215             # parse block-level syntaxes
216 595         604 my ($i, @output, $tail, $prev_i);
217 595         1338 OUTER_LINE: for ($i=0; $i<@lines;) {
218 2095 50 66     7227 die "_nimble_parse: no rule advanced line index for lines[$i]" if defined $prev_i && $i == $prev_i;
219 2095         2362 $prev_i = $i;
220              
221 2095         3372 foreach my $extra_rule (@extra_rules) {
222 95         127 my ($extra_re, $extra_fn) = @$extra_rule;
223 95         94 my @extra_matches;
224 95 100       593 if (@extra_matches = $lines[$i] =~ $extra_re) {
225 30         76 push @output, $extra_fn->(\@extra_matches, \$i, \@lines);
226 30         130 next OUTER_LINE;
227             }
228             }
229              
230 2065 100       24999 if (0) {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
231             # metadata
232 0         0 } elsif (my ($meta_key, $meta_value) = $lines[$i] =~ /^\@(\w+)(?:\s+(.*?))?\s*$/) {
233 5   50     25 $meta{$meta_key} = $meta_value // "";
234 5         17 $i++;
235             # codeblock
236             } elsif (my ($codeblock_marker, $codeblock_lang) = $lines[$i] =~ /^(\`{3,})(?:\s*(\w+))?\s*$/) {
237 15         54 my $node = {type=>"codeblock", lang=>$codeblock_lang, lines=>[]};
238 15         19 $i++;
239              
240 15         60 for (; $i<@lines; $i++) {
241 70 100       351 if ($lines[$i] =~ /^\Q$codeblock_marker\E\s*$/) {
242 15         18 $i++; last;
  15         25  
243             } else {
244 55         58 push @{$node->{lines}}, $lines[$i];
  55         203  
245             }
246             }
247 15         48 push @output, $node;
248             # ul
249             } elsif (my ($ul_indent, $ul_content) = $lines[$i] =~ /^\*( +)(.*?)$/) {
250 105         362 my $node = {type=>"ul", list=>[[$ul_content]]};
251 105         146 $i++;
252 105         239 for (; $i<@lines; $i++) {
253 340 100       2213 if (($tail) = $lines[$i] =~ /^ $ul_indent(.*?)$/) {
    100          
254 150         175 push @{$node->{list}[-1]}, $tail;
  150         577  
255             } elsif (($ul_indent, $ul_content) = $lines[$i] =~ /^\*( +)(.*?)$/) {
256 140         164 push @{$node->{list}}, [$ul_content];
  140         650  
257             } else {
258 50         88 last;
259             }
260             }
261 105         122 $node->{list} = [ map { _nimble_parse($_) } @{$node->{list}} ];
  245         703  
  105         179  
262 105         512 push @output, $node;
263             # ol
264             } elsif (my ($ol_indent, $ol_value, $ol_content) = $lines[$i] =~ /^((?:(\d+)|\#)\. +)(.*?)$/) {
265 25         134 my $node = {type=>"ol", content=>[{type=>"li", value=>$ol_value, content=>[$ol_content]}]};
266 25         41 $i++;
267 25         42 $ol_indent = " " x length $ol_indent;
268 25         63 for (; $i<@lines; $i++) {
269 70 100       603 if (($tail) = $lines[$i] =~ /^$ol_indent(.*?)$/) {
    100          
270 35         42 push @{$node->{content}[-1]{content}}, $tail;
  35         161  
271             } elsif (($ol_value, $ol_content) = $lines[$i] =~ /^(?:(\d+)|\#)\. (.*?)$/) {
272 30         38 push @{$node->{content}}, {type=>"li", value=>$ol_value, content=>[$ol_content]};
  30         124  
273 30 100       156 $ol_indent = " " x ((defined $ol_value ? length $ol_value : 1) + 2);
274             } else {
275 5         11 last;
276             }
277             }
278 25         30 $_->{content} = _nimble_parse($_->{content}) for @{$node->{content}};
  25         78  
279 25         107 push @output, $node;
280             # dl
281             } elsif (my ($dl_indent, $dl_content) = $lines[$i] =~ /^\?( +)(.*?)$/) {
282 40         175 my $node = {type=>"dl", content=>[{type=>"dt",content=>[$dl_content]}]};
283 40         54 $i++;
284 40         90 for (; $i<@lines; $i++) {
285 180 100       1228 if (($tail) = $lines[$i] =~ /^ $dl_indent(.*?)$/) {
    100          
    100          
286 50         54 push @{$node->{content}[-1]{content}}, $tail;
  50         195  
287             } elsif (($dl_indent, $dl_content) = $lines[$i] =~ /^\=( +)(.*?)$/) {
288 75         81 push @{$node->{content}}, {type=>"dd",content=>[$dl_content]};
  75         435  
289             } elsif (($dl_indent, $dl_content) = $lines[$i] =~ /^\?( +)(.*?)$/) {
290 35         39 push @{$node->{content}}, {type=>"dt",content=>[$dl_content]};
  35         231  
291             } else {
292 20         38 last;
293             }
294             }
295 40         48 $_->{content} = _nimble_parse($_->{content}) for @{$node->{content}};
  40         130  
296 40         164 push @output, $node;
297             # figure / figcaption
298             } elsif (my ($fig_indent, $fig_content) = $lines[$i] =~ /^\%( +)(.*?)$/) {
299 35         111 my $node = {type=>"figure", content=>[$fig_content]};
300 35         43 $i++;
301              
302 35         77 for (; $i<@lines; $i++) {
303 135 100       733 if (($tail) = $lines[$i] =~ /^ $fig_indent(.*?)$/) {
304 100         105 push @{$node->{content}}, $tail;
  100         391  
305             } else {
306 35         56 last;
307             }
308             }
309              
310             $node->{content} = _nimble_parse($node->{content}, [
311             [qr/^\=( +)(.*?)$/ => sub {
312 30         45 my ($matches, $i, $lines) = @_;
313 30         53 my ($figcap_indent, $figcap_content) = @$matches;
314 30         88 my $node = {type=>"figcaption", content=>[$figcap_content]};
315 30         35 $$i++;
316              
317 30         29 my $tail;
318 30         68 for (; $$i<@$lines; $$i++) {
319 10 50       125 if (($tail) = $lines->[$$i] =~ /^ $figcap_indent(.*?)$/) {
320 0         0 push @{$node->{content}}, $tail;
  0         0  
321             } else {
322 10         19 last;
323             }
324             }
325              
326 30         61 $node->{content} = _nimble_parse($node->{content});
327 30         75 return $node;
328 35         256 }],
329             ]);
330              
331 35         366 push @output, $node;
332             # h#
333             } elsif (my ($h_indent, $h_depth, $h_content) = $lines[$i] =~ /^(\!([123456]?) +)(.*?)$/) {
334 10   50     65 my $node = {type=>"h", content=>[$h_content], depth=>$h_depth||1};
335 10         12 $i++;
336              
337 10         22 $h_indent = " " x length $h_indent;
338 10         27 for (; $i<@lines; $i++) {
339 20 100       197 if (($tail) = $lines[$i] =~ /^$h_indent(.*?)$/) {
340 10         11 push @{$node->{content}}, $tail;
  10         42  
341             } else {
342 10         15 last;
343             }
344             }
345              
346 10         16 $node->{content} = _nimble_parse_inline(join(" ", @{$node->{content}}));
  10         37  
347 10         45 push @output, $node;
348             # blockquote
349             } elsif (my ($bq_indent, $bq_content) = $lines[$i] =~ /^\"( +)(.*?)$/) {
350 20         74 my $node = {type=>"blockquote", quote=>[$bq_content]};
351 20         27 $i++;
352              
353 20         26 my $section = 'quote';
354 20         48 for (; $i<@lines; $i++) {
355 90 100 100     607 if (($bq_content) = $lines[$i] =~ /^ $bq_indent(.*?)$/) {
    100          
356 65         78 push @{$node->{$section}}, $bq_content;
  65         256  
357             } elsif ($section eq 'quote' && (($bq_indent, $bq_content) = $lines[$i] =~ /^\-( +)(.*?)$/)) {
358 5         10 $section = 'cite';
359 5         25 $node->{$section} = [$bq_content];
360             } else {
361 20         35 last;
362             }
363             }
364              
365 20         30 $node->{$_} = _nimble_parse($node->{$_}) for grep { defined $node->{$_} } qw(quote cite);
  40         119  
366 20         82 push @output, $node;
367             # raw data
368             } elsif (my ($raw_format, $raw_inline) = $lines[$i] =~ /^\#raw\s+(\w+)(?:\s+(.*?))?\s*$/) {
369 10         53 my $node = {type=>"raw", format=>$raw_format, content=>[]};
370 10         16 $i++;
371              
372 10 100       19 if (defined $raw_inline) {
373 5         10 $node->{content} = $raw_inline;
374             } else {
375 5         17 for (; $i<@lines; $i++) {
376 35 100       179 if (my ($raw_line) = $lines[$i] =~ /^ +(.*?)\s*$/) {
377 30         35 push @{$node->{content}}, $raw_line;
  30         107  
378             } else {
379 5         7 last;
380             }
381             }
382 5         8 $node->{content} = join("\n", @{$node->{content}});
  5         17  
383             }
384              
385 10         44 push @output, $node;
386             # macro definition
387             } elsif (my ($macro_decl) = $lines[$i] =~ /^\#macro\s+(\w+)\s*$/) {
388 55         164 my $macro = {args=>{}, results=>{}};
389 55         69 $i++;
390              
391 55         58 my @errors;
392 55 100       131 push @errors, "a macro named '$macro_decl' already exists" if $macro{$macro_decl};
393             # we're building $macro as a temp variable, so no need to skip altering logic based on @errors
394              
395 55         54 my ($block_type, $block_name, $block_value, $block_indent, $block_internal_indent);
396 55         4846 for (; $i<@lines; $i++) {
397 240 100 100     2895 if (defined $block_type && (my ($block_line) = $lines[$i] =~ /^$block_indent( +.*?)\s*$/)) {
    100          
    100          
398 45 100       99 if (!defined $block_internal_indent) {
399 25         50 $block_line =~ /^( +)/;
400 25         48 $block_internal_indent = $1;
401             }
402 45         165 $block_line =~ s/^$block_internal_indent//;
403 45         63 push @{$macro->{$block_type}{$block_name}{$block_value}}, $block_line;
  45         214  
404             } elsif (my ($arg_indent, $arg_name, $arg_type, $arg_inline_default) = $lines[$i] =~ /^( +)\#arg\s+(\w+)\s+(\w+)(?:\s+(\S.*?))?\s*$/) {
405 100 100       250 push @errors, "duplicate definition for argument '$arg_name'" if $macro->{args}{$arg_name};
406 100 100       346 push @errors, "invalid type '$arg_type' for argument '$arg_name'" unless $arg_type =~ /^(?:nimble|raw)$/;
407 100         547 $macro->{args}{$arg_name} = {type=>$arg_type, default=>[]};
408 100 100       178 if (defined $arg_inline_default) {
409 15         36 $macro->{args}{$arg_name}{default} = $arg_inline_default;
410 15         57 $block_type = undef;
411             } else {
412 85         521 ($block_type, $block_name, $block_value, $block_indent, $block_internal_indent) = ("args", $arg_name, "default", $arg_indent, undef);
413             }
414             } elsif (my ($result_indent, $result_format, $result_inline_default) = $lines[$i] =~ /^( +)\#result\s+(\w+)(?:\s+(\S.*?))?\s*$/) {
415 40 100       107 push @errors, "duplicate definition of '$result_format'-format result" if $macro->{results}{$result_format};
416 40         123 $macro->{results}{$result_format} = {output=>[]};
417 40 100       84 if (defined $result_inline_default) {
418 20         38 $macro->{results}{$result_format}{output} = $result_inline_default;
419 20         67 $block_type = undef;
420             } else {
421 20         78 ($block_type, $block_name, $block_value, $block_indent, $block_internal_indent) = ("results", $result_format, "output", $result_indent, undef);
422             }
423             } else {
424 55         90 last;
425             }
426             }
427              
428 55 100       101 if (!@errors) {
429 30         30 foreach my $arg_name (keys %{$macro->{args}}) {
  30         97  
430 70         204 $macro->{args}{$arg_name}{default} = _macro_arg_preprocess($macro->{args}{$arg_name}{type}, $macro->{args}{$arg_name}{default});
431             }
432 30         53 foreach my $result_format (keys %{$macro->{results}}) {
  30         83  
433 30 100       93 $macro->{results}{$result_format}{output} = join("\n", @{$macro->{results}{$result_format}{output}}) if ref $macro->{results}{$result_format}{output};
  20         102  
434             }
435              
436 30         166 $macro{$macro_decl} = $macro;
437             } else {
438 25         123 my $node = {type=>"error", context=>"defining macro '$macro_decl'", errors=>\@errors};
439 25         47 push @error, $node;
440 25         127 push @output, $node;
441             }
442             # macro usage
443             } elsif (my ($macro_name) = $lines[$i] =~ /^\$\s*(\w+)\s*$/) {
444 45         154 my $node = {type=>"macro", macro=>$macro_name, args=>{}};
445 45         55 $i++;
446              
447 45         50 my @errors;
448 45 100       114 push @errors, "no macro by that name is defined" unless $macro{$macro_name};
449              
450 45         45 my ($block_indent, $arg_block_name, $block_internal_indent);
451 45         96 for (; $i<@lines; $i++) {
452 190 100 100     1278 if (defined $arg_block_name && (my ($block_line) = $lines[$i] =~ /^$block_indent( +.*?)\s*$/)) {
    100          
453 25 100       53 if (!defined $block_internal_indent) {
454 10         21 $block_line =~ /^( +)/;
455 10         23 $block_internal_indent = $1;
456             }
457 25         99 $block_line =~ s/^$block_internal_indent//;
458 25 50       59 if (!@errors) {
459 25         27 push @{$node->{args}{$arg_block_name}}, $block_line;
  25         124  
460             }
461             } elsif (my ($arg_indent, $arg_name, $arg_inline_default) = $lines[$i] =~ /^( +)(\w+)\:(?:\s+(\S.*?))?\s*$/) {
462 120 100       330 push @errors, "duplicate argument '$arg_name'" if $node->{args}{$arg_name};
463 120 100       338 push @errors, "unknown argument '$arg_name'" unless $macro{$macro_name}{args}{$arg_name};
464 120 100       237 if (!@errors) {
465 65         148 $node->{args}{$arg_name} = [];
466             }
467 120 100       221 if (defined $arg_inline_default) {
468 110 100       184 if (!@errors) {
469 55         111 $node->{args}{$arg_name} = $arg_inline_default;
470             }
471 110         356 $arg_block_name = undef;
472             } else {
473 10         39 ($arg_block_name, $block_indent, $block_internal_indent) = ($arg_name, $arg_indent, undef);
474             }
475             } else {
476 45         71 last;
477             }
478             }
479              
480 45 100       87 if (!@errors) {
481 15         30 _macro_invocation_preprocess($macro_name, $node);
482             }
483              
484 45 100       111 if (@errors) {
485 30         120 $node = {type=>"error", context=>"building block macro '$macro_name' invocation", errors=>\@errors};
486 30         92 push @error, $node;
487             }
488 45         191 push @output, $node;
489             # space
490             } elsif ($lines[$i] =~ /^\s*$/) {
491 655 100 100     3747 push @output, {type=>"space"} unless @output && $output[-1]{type} eq 'space';
492 655         2205 $i++;
493             # section start/end markers; below 'space' so at least one half of the regex matches
494             } elsif (my ($section_end, $header_indent, $header_content) = $lines[$i] =~ /^(\}*)(?:(\{ +)(.*?))?$/) {
495 55         64 $i++;
496 55 50       106 if (defined $section_end) {
497 55         71 my $section_end_num = length($section_end);
498 55 50       104 if ($section_end_num > $section_depth) {
499 0         0 my $node = {type=>"error", context=>"processing section end makers", errors=>["tried to end a section which wasn't open"]};
500 0         0 push @error, $node;
501 0         0 push @output, $node;
502 0         0 $section_end_num = $section_depth;
503             }
504 55         165 push @output, {type=>"section_end"} for 1..$section_end_num;
505 55         81 $section_depth -= $section_end_num;
506             }
507 55 100       164 if (defined $header_content) {
508 30         96 my $node = {type=>"section_start", content=>[$header_content]};
509              
510 30 50       84 $header_indent = " " x (length($header_indent) + (defined $section_end ? length($section_end) : 0));
511 30         88 for (; $i<@lines; $i++) {
512 55 100       370 if (($tail) = $lines[$i] =~ /^$header_indent(.*?)$/) {
513 25         27 push @{$node->{content}}, $tail;
  25         108  
514             } else {
515 30         48 last;
516             }
517             }
518              
519 30         67 $node->{content} = _nimble_parse($node->{content});
520 30         82 push @output, $node;
521 30         5752 $section_depth++;
522             }
523             # paragraph
524             } else {
525 990         1314 my $line = $lines[$i];
526 990         1573 $line =~ s/^\s+//; $line =~ s/\s+$//;
  990         1973  
527 990 100 100     3421 if (@output && $output[-1]{type} eq 'paragraph') {
528 105         241 $output[-1]{content} .= " $line";
529             } else {
530 885         2532 push @output, {type=>"paragraph", content=>$line};
531             }
532 990         3682 $i++;
533             }
534             }
535              
536             # clean up remaining sections
537 595         1066 push @output, {type=>"section_end"} for 1..$section_depth;
538              
539             # drop spaces; they were just to delimit paragraphs
540 595         755 @output = grep {$_->{type} ne 'space'} @output;
  1930         4419  
541              
542             # parse text which has been collected into paragraphs
543 595         808 $_->{content} = _nimble_parse_inline($_->{content}) for grep {$_->{type} eq 'paragraph'} @output;
  1305         3141  
544              
545 595         2564 return \@output;
546 5         124 };
547              
548 5         11 do {
549 5         21 my $lines = _nimble_makelines($_[0]);
550 5         19 my $tree = _nimble_parse($lines);
551 5 50       1257 return {tree=>$tree, meta=>\%meta, macro=>\%macro, (@error ? (error=>\@error) : ())};
552             };
553             }
554              
555             sub _renderer_html {
556 4 50   4   13 croak "Text::Nimble::_renderer_html takes one argument but got ".(scalar @_) unless @_ == 1;
557 4 50 33     49 croak "Text::Nimble::_renderer_html got an argument that doesn't look like the result of Text::Nimble::parse" unless ref $_[0] eq 'HASH' && $_[0]{tree} && $_[0]{macro};
      33        
558              
559 4         7 my %macro = %{$_[0]{macro}};
  4         33  
560              
561 4         23 my %xmlenc = (
562             '&' => '&',
563             '"' => '"',
564             '<' => '<',
565             '>' => '>',
566             );
567             local *_xmlenc = sub {
568 1608     1608   2085 my $s = $_[0];
569 1608         2539 $s =~ s/([\&\"\<\>])/$xmlenc{$1}/g;
570 1608         8534 return $s;
571 4         21 };
572              
573             local *_urlenc = sub {
574 16     16   20 my $s = $_[0];
575 16         39 $s =~ s/([^a-zA-Z0-9\ ])/sprintf("%%%02X",ord($1))/ge;
  4         38  
576 16         32 $s =~ s/\ /\+/g;
577 16         56 return $s;
578 4         16 };
579              
580 4         44 my %macro_filters = (
581             xmlenc => \&_xmlenc,
582             urlenc => \&_urlenc,
583             );
584              
585             local *_macro_var_interpolate = sub {
586 100     100   202 my ($macro, $arg_name, $filters) = @_;
587 100 100       293 my @filters = defined $filters ? split(/\|/, $filters) : ();
588 100         137 my @unknown_filters = grep {!$macro_filters{$_}} @filters;
  104         242  
589 100 50       206 return "[nimble macro error: unknown argument filters(s): " . join(", ", @unknown_filters) . "]" if @unknown_filters;
590 100         193 my $value = $macro->{args}{$arg_name};
591 100         224 $value = $macro_filters{$_}($value) for @filters;
592 100         450 return $value;
593 4         19 };
594              
595             local *_render_html = sub {
596 3876     3876   4473 my $node = shift;
597              
598             # handle a list of nodes, especially the root level of Text::Nimble::parse
599 3876 100       8115 if (ref $node eq 'ARRAY') {
600             # a lone paragraph should just be plain text instead
601 1420 100 100     5721 if (@$node == 1 && $node->[0]{type} eq 'paragraph') {
602 324         663 return _render_html($node->[0]{content});
603             } else {
604 1096 50       2620 return join("", map { _render_html($_) } @$node) if ref $node eq 'ARRAY';
  2456         4325  
605             }
606             }
607              
608 2456         3633 my $type = $node->{type};
609 2456 100       18033 if (0) {}
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
610 0         0 elsif ($type eq 'paragraph' ) { return "

" ._render_html($node->{content})."" }

  360         670  
611 32         64 elsif ($type eq 'dl' ) { return "
" ._render_html($node->{content})."" }
612 60         118 elsif ($type eq 'dt' ) { return "
" ._render_html($node->{content})."
" }
613 60         123 elsif ($type eq 'dd' ) { return "
" ._render_html($node->{content})."
" }
614 80         171 elsif ($type eq 'emphasis' ) { return "" ._render_html($node->{content})."" }
615 20         45 elsif ($type eq 'ol' ) { return "
    " ._render_html($node->{content})."" }
616 28         54 elsif ($type eq 'figure' ) { return "
"._render_html($node->{content})."" }
617 24         50 elsif ($type eq 'figcaption' ) { return "
"._render_html($node->{content})."" }
618 24         78 elsif ($type eq 'h' ) { return "{depth}>"._render_html($node->{content})."{depth}\n>" }
619 84         191 elsif ($type eq 'strong' ) { return ""._render_html($node->{content})."" }
620 32         63 elsif ($type eq 'link' ) { return "{url})."\">"._render_html($node->{content})."" }
621 20         40 elsif ($type eq 'img' ) { return "{src})."\" alt=\""._xmlenc($node->{alt})."\"/>" }
622 44 100       154 elsif ($type eq 'li' ) { return "{value}?" value=\"$node->{value}\"":"").">"._render_html($node->{content})."" }
623 56         105 elsif ($type eq 'code' ) { return ""._xmlenc($node->{text})."" }
624 12         18 elsif ($type eq 'codeblock' ) { return "
".join("\n", map{_xmlenc($_)} @{$node->{lines}})."" } 
  44         66  
  12         29  
625 1184         2231 elsif ($type eq 'text' ) { return _xmlenc($node->{text}) }
626 72         199 elsif ($type eq 'entity' ) { return $node->{html} }
627 80         100 elsif ($type eq 'ul' ) { return "
    ".join("", map{"
  • "._render_html($_)."
  • "} @{$node->{list}})."" }
  188         356  
  80         146  
628 12 50       45 elsif ($type eq 'raw' ) { return $node->{format} eq 'html' ? $node->{content} : '' }
629 68         133 elsif ($type eq 'error' ) { return "Nimble error while "._xmlenc($node->{context}).": ".join("; ", map{_xmlenc($_)} @{$node->{errors}})."" }
  96         144  
  68         133  
630             elsif ($type eq 'blockquote' ) {
631 16         36 my $bq_html = "
"._render_html($node->{quote})."";
632 16 100       56 $bq_html = "
$bq_html
"._render_html($node->{cite})."
" if $node->{cite};
633 16         42 return $bq_html;
634             }
635             elsif ($type eq 'section_start') {
636 24         60 return ""
637             . (@{$node->{content}}
638             ? "
"._render_html(
639 16         107 @{$node->{content}} == 1 && $node->{content}[0]{type} eq 'paragraph'
640 24 100 66     31 ? [{%{$node->{content}[0]}, type=>"h", depth=>1}]
    50          
641             : $node->{content}
642             ).""
643             : ""
644             )
645             }
646 24         61 elsif ($type eq 'section_end') { return "" }
647             elsif ($type eq 'macro') {
648 40 50       144 return "" unless $macro{$node->{macro}}{results}{html};
649              
650 40         45 $node->{args}{$_} = _render_html($node->{args}{$_}) for grep {ref $node->{args}{$_}} keys %{$node->{args}};
  84         223  
  40         121  
651              
652 40         136 my $output = $macro{$node->{macro}}{results}{html}{output};
653 40         194 $output =~ s/\{\{(\w+)(?:\|(\w+(?:\|\w+)*))?\}\}/_macro_var_interpolate($node, $1, $2)/ge;
  100         183  
654 40         179 return $output;
655             }
656 4         115 };
657              
658 4         8 do {
659 4         6 my $parse = $_[0];
660 4         12 return _render_html($parse->{tree});
661             };
662             }
663              
664             1; # End of Text::Nimble
665              
666             __END__