File Coverage

blib/lib/Game/TextMapper.pm
Criterion Covered Total %
statement 98 160 61.2
branch 4 40 10.0
condition 14 93 15.0
subroutine 26 29 89.6
pod 0 7 0.0
total 142 329 43.1


line stmt bran cond sub pod time code
1             #!/usr/bin/env perl
2             # Copyright (C) 2009-2022 Alex Schroeder
3             #
4             # This program is free software: you can redistribute it and/or modify it under
5             # the terms of the GNU Affero General Public License as published by the Free
6             # Software Foundation, either version 3 of the License, or (at your option) any
7             # later version.
8             #
9             # This program is distributed in the hope that it will be useful, but WITHOUT
10             # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11             # FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
12             # details.
13             #
14             # You should have received a copy of the GNU Affero General Public License along
15             # with this program. If not, see .
16              
17             package Game::TextMapper;
18              
19             our $VERSION = 1.05;
20              
21 1     1   1215 use Game::TextMapper::Log;
  1         2  
  1         26  
22 1     1   438 use Game::TextMapper::Point;
  1         2  
  1         7  
23 1     1   457 use Game::TextMapper::Line;
  1         2  
  1         14  
24 1     1   429 use Game::TextMapper::Mapper::Hex;
  1         3  
  1         13  
25 1     1   480 use Game::TextMapper::Mapper::Square;
  1         3  
  1         8  
26 1     1   460 use Game::TextMapper::Smale;
  1         3  
  1         8  
27 1     1   476 use Game::TextMapper::Apocalypse;
  1         2  
  1         6  
28 1     1   573 use Game::TextMapper::Gridmapper;
  1         3  
  1         15  
29 1     1   577 use Game::TextMapper::Schroeder::Alpine;
  1         3  
  1         6  
30 1     1   457 use Game::TextMapper::Schroeder::Archipelago;
  1         12  
  1         5  
31 1     1   485 use Game::TextMapper::Schroeder::Island;
  1         3  
  1         5  
32 1     1   530 use Game::TextMapper::Traveller;
  1         4  
  1         8  
33              
34 1     1   35 use Modern::Perl '2018';
  1         1  
  1         3  
35 1     1   643 use Mojolicious::Lite;
  1         56703  
  1         7  
36 1     1   24319 use Mojo::DOM;
  1         3  
  1         26  
37 1     1   4 use Mojo::Util qw(url_escape xml_escape);
  1         2  
  1         43  
38 1     1   5 use File::ShareDir 'dist_dir';
  1         3  
  1         33  
39 1     1   1568 use Pod::Simple::HTML;
  1         14649  
  1         33  
40 1     1   1160 use Pod::Simple::Text;
  1         6533  
  1         74  
41 1     1   8 use List::Util qw(none);
  1         1  
  1         73  
42 1     1   7 use Cwd;
  1         2  
  1         6026  
43              
44             # Commands for the command line!
45             push @{app->commands->namespaces}, 'Game::TextMapper::Command';
46              
47             # Change scheme if "X-Forwarded-Proto" header is set (presumably to HTTPS)
48             app->hook(before_dispatch => sub {
49             my $c = shift;
50             $c->req->url->base->scheme('https')
51             if $c->req->headers->header('X-Forwarded-Proto') } );
52              
53             plugin Config => {
54             default => {
55             loglevel => 'warn',
56             logfile => undef,
57             contrib => undef,
58             },
59             file => getcwd() . '/text-mapper.conf',
60             };
61              
62             my $log = Game::TextMapper::Log->get;
63             $log->level(app->config('loglevel'));
64             $log->path(app->config('logfile'));
65             $log->info($log->path ? "Logfile is " . $log->path : "Logging to stderr");
66              
67             my $dist_dir = app->config('contrib') // dist_dir('Game-TextMapper');
68             $log->debug("Reading contrib files from $dist_dir");
69              
70             get '/' => sub {
71             my $c = shift;
72             my $param = $c->param('map');
73             if ($param) {
74             my $mapper;
75             if ($c->param('type') and $c->param('type') eq 'square') {
76             $mapper = Game::TextMapper::Mapper::Square->new(dist_dir => $dist_dir);
77             } else {
78             $mapper = Game::TextMapper::Mapper::Hex->new(dist_dir => $dist_dir);
79             }
80             $mapper->initialize($param);
81             $c->render(text => $mapper->svg, format => 'svg');
82             } else {
83             my $mapper = new Game::TextMapper::Mapper;
84             my $map = $mapper->initialize('')->example();
85             $c->render(template => 'edit', map => $map);
86             }
87             };
88              
89             any '/edit' => sub {
90             my $c = shift;
91             my $mapper = new Game::TextMapper::Mapper;
92             my $map = $c->param('map') || $mapper->initialize('')->example();
93             $c->render(map => $map);
94             };
95              
96             any '/render' => sub {
97             my $c = shift;
98             my $mapper;
99             if ($c->param('type') and $c->param('type') eq 'square') {
100             $mapper = Game::TextMapper::Mapper::Square->new(dist_dir => $dist_dir);
101             } else {
102             $mapper = Game::TextMapper::Mapper::Hex->new(dist_dir => $dist_dir);
103             }
104             $mapper->initialize($c->param('map'));
105             $c->render(text => $mapper->svg, format => 'svg');
106             };
107              
108             get '/:type/redirect' => sub {
109             my $self = shift;
110             my $type = $self->param('type');
111             my $rooms = $self->param('rooms');
112             my $seed = $self->param('seed');
113             my $caves = $self->param('caves');
114             my %params = ();
115             $params{rooms} = $rooms if $rooms;
116             $params{seed} = $seed if $seed;
117             $params{caves} = $caves if $caves;
118             $self->redirect_to($self->url_for($type . "random")->query(%params));
119             } => 'redirect';
120              
121             # alias for /smale
122             get '/random' => sub {
123             my $c = shift;
124             my $bw = $c->param('bw');
125             my $width = $c->param('width');
126             my $height = $c->param('height');
127             $c->render(template => 'edit', map => Game::TextMapper::Smale->new->generate_map($width, $height, $bw));
128             };
129              
130             get '/smale' => sub {
131             my $c = shift;
132             my $bw = $c->param('bw');
133             my $width = $c->param('width');
134             my $height = $c->param('height');
135             if ($c->stash('format')||'' eq 'txt') {
136             $c->render(text => Game::TextMapper::Smale->new->generate_map($width, $height));
137             } else {
138             $c->render(template => 'edit',
139             map => Game::TextMapper::Smale->new->generate_map($width, $height, $bw));
140             }
141             };
142              
143             get '/smale/random' => sub {
144             my $c = shift;
145             my $bw = $c->param('bw');
146             my $width = $c->param('width');
147             my $height = $c->param('height');
148             my $map = Game::TextMapper::Smale->new->generate_map($width, $height, $bw);
149             my $svg = Game::TextMapper::Mapper::Hex->new(dist_dir => $dist_dir)
150             ->initialize($map)
151             ->svg();
152             $c->render(text => $svg, format => 'svg');
153             };
154              
155             get '/smale/random/text' => sub {
156             my $c = shift;
157             my $bw = $c->param('bw');
158             my $width = $c->param('width');
159             my $height = $c->param('height');
160             my $text = Game::TextMapper::Smale->new->generate_map($width, $height, $bw);
161             $c->render(text => $text, format => 'txt');
162             };
163              
164             sub alpine_map {
165 2     2 0 4 my $c = shift;
166             # must be able to override this for the documentation
167 2   33     13 my $step = shift // $c->param('step');
168             # need to compute the seed here so that we can send along the URL
169 2   33     488 my $seed = $c->param('seed') || int(rand(1000000000));
170 2         117 my $url = $c->url_with('alpinedocument')->query({seed => $seed})->to_abs;
171 2         1330 my @params = ($c->param('width'),
172             $c->param('height'),
173             $c->param('steepness'),
174             $c->param('peaks'),
175             $c->param('peak'),
176             $c->param('bumps'),
177             $c->param('bump'),
178             $c->param('bottom'),
179             $c->param('arid'),
180             $c->param('wind'),
181             $seed,
182             $url,
183             $step,
184             );
185 2   100     867 my $type = $c->param('type') // 'hex';
186 2 100       96 if ($type eq 'hex') {
187 1         17 return Game::TextMapper::Schroeder::Alpine
188             ->with_roles('Game::TextMapper::Schroeder::Hex')->new()
189             ->generate_map(@params);
190             } else {
191 1         16 return Game::TextMapper::Schroeder::Alpine
192             ->with_roles('Game::TextMapper::Schroeder::Square')->new()
193             ->generate_map(@params);
194             }
195             }
196              
197             get '/alpine' => sub {
198             my $c = shift;
199             my $map = alpine_map($c);
200             if ($c->stash('format') || '' eq 'txt') {
201             $c->render(text => $map);
202             } else {
203             $c->render(template => 'edit', map => $map);
204             }
205             };
206              
207             get '/alpine/random' => sub {
208             my $c = shift;
209             my $map = alpine_map($c);
210             my $type = $c->param('type') // 'hex';
211             my $mapper;
212             if ($type eq 'hex') {
213             $mapper = Game::TextMapper::Mapper::Hex->new(dist_dir => $dist_dir);
214             } else {
215             $mapper = Game::TextMapper::Mapper::Square->new(dist_dir => $dist_dir);
216             }
217             my $svg = $mapper->initialize($map)->svg;
218             $c->render(text => $svg, format => 'svg');
219             };
220              
221             get '/alpine/random/text' => sub {
222             my $c = shift;
223             my $map = alpine_map($c);
224             $c->render(text => $map, format => 'txt');
225             };
226              
227             get '/alpine/document' => sub {
228             my $c = shift;
229             # prepare a map for every step
230             my @maps;
231             my $type = $c->param('type') || 'hex';
232             # use the same seed for all the calls
233             my $seed = $c->param('seed');
234             if (not defined $seed) {
235             $seed = int(rand(1000000000));
236             $c->param('seed' => $seed);
237             }
238             for my $step (1 .. 18) {
239             my $map = alpine_map($c, $step);
240             my $mapper;
241             if ($type eq 'hex') {
242             $mapper = Game::TextMapper::Mapper::Hex->new(dist_dir => $dist_dir);
243             } else {
244             $mapper = Game::TextMapper::Mapper::Square->new(dist_dir => $dist_dir);
245             }
246             my $svg = $mapper->initialize($map)->svg;
247             $svg =~ s/<\?xml version="1.0" encoding="UTF-8" standalone="no"\?>\n//g;
248             push(@maps, $svg);
249             };
250             $c->stash("maps" => \@maps);
251              
252             # The documentation needs all the defaults of Alpine::generate_map (but
253             # we'd like to use a smaller map because it is so slow).
254             my $width = $c->param('width') // 20;
255             my $height = $c->param('height') // 5; # instead of 10
256             my $steepness = $c->param('steepness') // 3;
257             my $peaks = $c->param('peaks') // int($width * $height / 40);
258             my $peak = $c->param('peak') // 10;
259             my $bumps = $c->param('bumps') // int($width * $height / 40);
260             my $bump = $c->param('bump') // 2;
261             my $bottom = $c->param('bottom') // 0;
262             my $arid = $c->param('arid') // 2;
263              
264             # Generate the documentation text based on the stashed maps.
265             $c->render(template => 'alpine_document',
266             seed => $seed,
267             width => $width,
268             height => $height,
269             steepness => $steepness,
270             peaks => $peaks,
271             peak => $peak,
272             bumps => $bumps,
273             bump => $bump,
274             bottom => $bottom,
275             arid => $arid);
276             };
277              
278             get '/alpine/parameters' => sub {
279             my $c = shift;
280             $c->render(template => 'alpine_parameters');
281             };
282              
283             # does not handle z coordinates
284             sub border_modification {
285 0     0 0 0 my ($map, $top, $left, $right, $bottom, $empty) = @_;
286 0         0 my (@lines, @temp, %seen);
287 0         0 my ($x, $y, $points, $text);
288 0         0 my ($minx, $miny, $maxx, $maxy);
289             # shift map around
290 0         0 foreach (split(/\r?\n/, $map)) {
291 0 0       0 if (($x, $y, $text) = /^(\d\d)(\d\d)\s+(.*)/) {
    0          
292 0 0 0     0 $minx = $x if not defined $minx or $x < $minx;
293 0 0 0     0 $miny = $y if not defined $miny or $y < $miny;
294 0 0 0     0 $maxx = $x if not defined $maxx or $x > $maxx;
295 0 0 0     0 $maxy = $y if not defined $maxy or $y > $maxy;
296 0         0 my $point = Game::TextMapper::Point->new(x => $x + $left, y => $y + $top);
297 0 0       0 $seen{$point->coordinates} = 1 if $empty;
298 0         0 push(@lines, [$point, $text]);
299             } elsif (($points, $text) = /^(-?\d\d-?\d\d(?:--?\d\d-?\d\d)+)\s+(.*)/) {
300 0         0 my @numbers = $points =~ /\G(-?\d\d)(-?\d\d)-?/cg;
301 0         0 my @points;
302 0         0 while (@numbers) {
303 0         0 my ($x, $y) = splice(@numbers, 0, 2);
304 0         0 push(@points, Game::TextMapper::Point->new(x => $x + $left, y => $y + $top));
305             }
306 0         0 push(@lines, [Game::TextMapper::Line->new(points => \@points), $text]);
307             } else {
308 0         0 push(@lines, $_);
309             }
310             }
311             # only now do we know the extent of the map
312 0         0 $maxx += $left + $right;
313 0         0 $maxy += $top + $bottom;
314             # with that information we can now determine what lies outside the map
315 0         0 @temp = ();
316 0         0 foreach (@lines) {
317 0 0       0 if (ref) {
318 0         0 my ($it, $text) = @$_;
319 0 0       0 if (ref($it) eq 'Game::TextMapper::Point') {
320 0 0 0     0 if ($it->x <= $maxx and $it->x >= $minx
      0        
      0        
321             and $it->y <= $maxy and $it->y >= $miny) {
322 0         0 push(@temp, $_);
323             }
324             } else { # Game::TextMapper::Line
325             my $outside = none {
326 0 0 0 0   0 ($_->x <= $maxx and $_->x >= $minx
      0        
327             and $_->y <= $maxy and $_->y >= $miny)
328 0         0 } @{$it->points};
  0         0  
329 0 0       0 push(@temp, $_) unless $outside;
330             }
331             } else {
332 0         0 push(@temp, $_);
333             }
334             }
335 0         0 @lines = @temp;
336             # add missing hexes, if requested
337 0 0       0 if ($empty) {
338 0         0 for $x ($minx .. $maxx) {
339 0         0 for $y ($miny .. $maxy) {
340 0         0 my $point = Game::TextMapper::Point->new(x => $x, y => $y);
341 0 0       0 if (not $seen{$point->coordinates}) {
342 0         0 push(@lines, [$point, "empty"]);
343             }
344             }
345             }
346             # also, sort regions before trails before others
347             @lines = sort {
348 0         0 (# arrays before strings
349 0 0 0     0 ref($b) cmp ref($a)
      0        
      0        
      0        
      0        
      0        
      0        
      0        
      0        
350             # string comparison if both are strings
351             or not(ref($a)) and not(ref($b)) and $a cmp $b
352             # if we get here, we know both are arrays
353             # points before lines
354             or ref($b->[0]) cmp ref($a->[0])
355             # if both are points, compare the coordinates
356             or ref($a->[0]) eq 'Game::TextMapper::Point' and $a->[0]->cmp($b->[0])
357             # if both are lines, compare the first two coordinates (the minimum line length)
358             or ref($a->[0]) eq 'Game::TextMapper::Line' and ($a->[0]->points->[0]->cmp($b->[0]->points->[0])
359             or $a->[0]->points->[1]->cmp($b->[0]->points->[1]))
360             # if bot are the same point (!) …
361             or 0)
362             } @lines;
363             }
364             $map = join("\n",
365             map {
366 0 0       0 if (ref) {
  0         0  
367 0         0 my ($it, $text) = @$_;
368 0 0       0 if (ref($it) eq 'Game::TextMapper::Point') {
369 0         0 Game::TextMapper::Point::coord($it->x, $it->y) . " " . $text
370             } else {
371 0         0 my $points = $it->points;
372             join("-",
373 0         0 map { Game::TextMapper::Point::coord($_->x, $_->y) } @$points)
  0         0  
374             . " " . $text;
375             }
376             } else {
377 0         0 $_;
378             }
379             } @lines) . "\n";
380 0         0 return $map;
381             }
382              
383             any '/borders' => sub {
384             my $c = shift;
385             my $map = border_modification(map { $c->param($_) } qw(map top left right bottom empty));
386             $c->param('map', $map);
387             $c->render(template => 'edit', map => $map);
388             };
389              
390             sub island_map {
391 2     2 0 5 my $c = shift;
392             # must be able to override this for the documentation
393 2   33     13 my $step = shift // $c->param('step');
394             # need to compute the seed here so that we can send along the URL
395 2   33     504 my $seed = $c->param('seed') || int(rand(1000000000));
396 2         110 my $url = $c->url_with('islanddocument')->query({seed => $seed})->to_abs;
397 2         1711 my @params = ($c->param('width'),
398             $c->param('height'),
399             $c->param('radius'),
400             $seed,
401             $url,
402             $step,
403             );
404 2   100     269 my $type = $c->param('type') // 'hex';
405 2 100       90 if ($type eq 'hex') {
406 1         22 return Game::TextMapper::Schroeder::Island
407             ->with_roles('Game::TextMapper::Schroeder::Hex')->new()
408             ->generate_map(@params);
409             } else {
410 1         11 return Game::TextMapper::Schroeder::Island
411             ->with_roles('Game::TextMapper::Schroeder::Square')->new()
412             ->generate_map(@params);
413             }
414             }
415              
416             get '/island' => sub {
417             my $c = shift;
418             my $map = island_map($c);
419             if ($c->stash('format') || '' eq 'txt') {
420             $c->render(text => $map);
421             } else {
422             $c->render(template => 'edit', map => $map);
423             }
424             };
425              
426             get '/island/random' => sub {
427             my $c = shift;
428             my $map = island_map($c);
429             my $type = $c->param('type') // 'hex';
430             my $mapper;
431             if ($type eq 'hex') {
432             $mapper = Game::TextMapper::Mapper::Hex->new(dist_dir => $dist_dir);
433             } else {
434             $mapper = Game::TextMapper::Mapper::Square->new(dist_dir => $dist_dir);
435             }
436             my $svg = $mapper->initialize($map)->svg;
437             $c->render(text => $svg, format => 'svg');
438             };
439              
440             sub archipelago_map {
441 0     0 0 0 my $c = shift;
442             # must be able to override this for the documentation
443 0   0     0 my $step = shift // $c->param('step');
444             # need to compute the seed here so that we can send along the URL
445 0   0     0 my $seed = $c->param('seed') || int(rand(1000000000));
446 0         0 my $url = $c->url_with('archipelagodocument')->query({seed => $seed})->to_abs;
447 0         0 my @params = ($c->param('width'),
448             $c->param('height'),
449             $c->param('concentration'),
450             $c->param('eruptions'),
451             $c->param('top'),
452             $c->param('bottom'),
453             $seed,
454             $url,
455             $step,
456             );
457 0   0     0 my $type = $c->param('type') // 'hex';
458 0 0       0 if ($type eq 'hex') {
459 0         0 return Game::TextMapper::Schroeder::Archipelago
460             ->with_roles('Game::TextMapper::Schroeder::Hex')->new()
461             ->generate_map(@params);
462             } else {
463 0         0 return Game::TextMapper::Schroeder::Archipelago
464             ->with_roles('Game::TextMapper::Schroeder::Square')->new()
465             ->generate_map(@params);
466             }
467             }
468              
469             get '/archipelago' => sub {
470             my $c = shift;
471             my $map = archipelago_map($c);
472             if ($c->stash('format') || '' eq 'txt') {
473             $c->render(text => $map);
474             } else {
475             $c->render(template => 'edit', map => $map);
476             }
477             };
478              
479             get '/archipelago/random' => sub {
480             my $c = shift;
481             my $map = archipelago_map($c);
482             my $type = $c->param('type') // 'hex';
483             my $mapper;
484             if ($type eq 'hex') {
485             $mapper = Game::TextMapper::Mapper::Hex->new(dist_dir => $dist_dir);
486             } else {
487             $mapper = Game::TextMapper::Mapper::Square->new(dist_dir => $dist_dir);
488             }
489             my $svg = $mapper->initialize($map)->svg;
490             $c->render(text => $svg, format => 'svg');
491             };
492              
493             sub gridmapper_map {
494 1     1 0 2 my $c = shift;
495 1   33     4 my $seed = $c->param('seed') || int(rand(1000000000));
496 1   50     201 my $pillars = $c->param('pillars') // 1;
497 1   50     52 my $rooms = $c->param('rooms') // 5;
498 1   50     46 my $caves = $c->param('caves') // 0;
499 1         43 srand($seed);
500 1         16 return Game::TextMapper::Gridmapper->new()
501             ->generate_map($pillars, $rooms, $caves);
502             }
503              
504             get '/gridmapper' => sub {
505             my $c = shift;
506             my $map = gridmapper_map($c);
507             if ($c->stash('format') || '' eq 'txt') {
508             $c->render(text => $map);
509             } else {
510             $c->render(template => 'edit', map => $map);
511             }
512             };
513              
514             get '/gridmapper/random' => sub {
515             my $c = shift;
516             my $map = gridmapper_map($c);
517             my $mapper = Game::TextMapper::Mapper::Square->new(dist_dir => $dist_dir);
518             my $svg = $mapper->initialize($map)->svg;
519             $c->render(text => $svg, format => 'svg');
520             };
521              
522             get '/gridmapper/random/text' => sub {
523             my $c = shift;
524             my $map = gridmapper_map($c);
525             $c->render(text => $map, format => 'txt');
526             };
527              
528             sub apocalypse_map {
529 1     1 0 2 my $c = shift;
530 1   33     5 my $seed = $c->param('seed') || int(rand(1000000000));
531 1         227 srand($seed);
532 1         3 my $hash = $c->req->params->to_hash;
533 1         54 return Game::TextMapper::Apocalypse->new(%$hash)
534             ->generate_map();
535             }
536              
537             get '/apocalypse' => sub {
538             my $c = shift;
539             my $map = apocalypse_map($c);
540             if ($c->stash('format') || '' eq 'txt') {
541             $c->render(text => $map);
542             } else {
543             $c->render(template => 'edit', map => $map);
544             }
545             };
546              
547             get '/apocalypse/random' => sub {
548             my $c = shift;
549             my $map = apocalypse_map($c);
550             my $mapper = Game::TextMapper::Mapper::Hex->new(dist_dir => $dist_dir);
551             my $svg = $mapper->initialize($map)->svg;
552             $c->render(text => $svg, format => 'svg');
553             };
554              
555             get '/apocalypse/random/text' => sub {
556             my $c = shift;
557             my $map = apocalypse_map($c);
558             $c->render(text => $map, format => 'txt');
559             };
560              
561             sub star_map {
562 1     1 0 3 my $c = shift;
563 1   33     5 my $seed = $c->param('seed') || int(rand(1000000000));
564 1         204 srand($seed);
565 1         4 my $hash = $c->req->params->to_hash;
566 1         41 return Game::TextMapper::Traveller->new(%$hash)->generate_map();
567             }
568              
569             get '/traveller' => sub {
570             my $c = shift;
571             my $map = star_map($c);
572             if ($c->stash('format') || '' eq 'txt') {
573             $c->render(text => $map);
574             } else {
575             $c->render(template => 'edit', map => $map);
576             }
577             };
578              
579             get '/traveller/random' => sub {
580             my $c = shift;
581             my $map = star_map($c);
582             my $mapper = Game::TextMapper::Mapper::Hex->new(dist_dir => $dist_dir);
583             my $svg = $mapper->initialize($map)->svg;
584             $c->render(text => $svg, format => 'svg');
585             };
586              
587             get '/traveller/random/text' => sub {
588             my $c = shift;
589             my $map = star_map($c);
590             $c->render(text => $map, format => 'txt');
591             };
592              
593             get '/help' => sub {
594             my $c = shift;
595              
596             seek(DATA,0,0);
597             local $/ = undef;
598             my $pod = ;
599             $pod =~ s/=head1 NAME\n.*=head1 DESCRIPTION/=head1 Text Mapper/gs;
600             my $parser = Pod::Simple::HTML->new;
601             $parser->html_header_after_title('');
602             $parser->html_header_before_title('');
603             $parser->title_prefix('');
605             my $html;
606             $parser->output_string(\$html);
607             $parser->parse_string_document($pod);
608              
609             my $dom = Mojo::DOM->new($html);
610             for my $pre ($dom->find('pre')->each) {
611             my $map = $pre->text;
612             $map =~ s/^ //mg;
613             next if $map =~ /^perl/; # how to call it
614             my $url = $c->url_for('render')->query(map => $map);
615             $pre->replace("
" . xml_escape($map) . "
\n"
616             . qq{

Render this example

});
617             }
618              
619             $c->render(html => $dom);
620             };
621              
622             app->start;
623              
624             __DATA__