File Coverage

blib/lib/Liveman.pm
Criterion Covered Total %
statement 168 227 74.0
branch 45 110 40.9
condition 12 59 20.3
subroutine 20 29 68.9
pod 8 8 100.0
total 253 433 58.4


line stmt bran cond sub pod time code
1             package Liveman;
2:
use 5.22.0;
3: use common::sense;
4:
5: our $VERSION = "1.0";
6:
7: use Term::ANSIColor qw/colored/;
8: use File::Slurper qw/read_text write_text/;
9: use Markdown::To::POD qw/markdown_to_pod/;
10:
11:
12: # Конструктор 13: sub new {
14: my $cls = shift;
15: my $self = bless {@_}, $cls;
16: delete $self->{files} if $self->{files} && !scalar @{$self->{files}};
17: $self
18: }
19:
20: # Пакет из пути
21: sub _pkg($) {
22: ( shift =~ s!^lib/(.*)\.\w+$!$1!r ) =~ s!/!::!gr
23: }
24:
25: # Переменная из пакета
26: sub _var($) {
27: '$' . lcfirst( shift =~ s!::(\w)?!_${\lc $1}!gr )
28: }
29:
30: # Для метода для вставки
31: sub _md_method(@) {
32: my ($pkg, $sub, $args, $remark) = @_;
33: my $sub_args = "$sub ($args)";
34: $args = "($args)" if $args;
35:
36: $remark = "." unless defined $remark;
37: my $var = _var $pkg;
38: << "END";
39: ## $sub_args
40:
41: $remark
42:
43: ```perl
44: my $var = $pkg->new;
45: ${var}->$sub$args # -> .3
46: ```
47:
48: END
49: }
50:
51: # Для фичи для вставки
52: sub _md_feature(@) {
53: my ($pkg, $has, $remark) = @_;
54:
55: $remark = "." unless defined $remark;
56: my $var = _var $pkg;
57: << "END";
58: ## $has
59:
60: $remark
61:
62: ```perl
63: my $var = $pkg->new;
64:
65: ${var}->$has\t# -> .5
66: ```
67:
68: END
69: }
70:
71:
72: # Добавить разделы функций в *.md из *.pm
73: sub appends {
74: my ($self) = @_;
75: my $files = $self->{files} // [split /\n/, `find lib -name '*.pm' -a -type f`];
76: $self->append($_) for @$files;
77: $self
78: }
79:
80: # Добавить разделы функций в *.md из *.pm
81: sub append {
82: my ($self, $pm) = @_;
83:
84: my $md = $pm =~ s!(\.\w+)?$!.md!r;
85:
86: die "Not file $pm!" if !-f $pm;
87: $self->mkmd($md) if !-f $md;
88:
89: local $_ = read_text $pm;
90: my %sub; my %has;
91: while(m! (^\# [\ \t]* (?<remark> .*?) [\ \t]* )? \n (
92: sub \s+ (?<sub> (\w+|::)+ ) .*
93: ( \s* my \s* \( \s* (\$self,? \s* )? (?<args>.*?) \s* \) \s* = \s* \@_; )?
94: | has \s+ (?<has> (\w+|'\w+'|"\w+"|\[ \s* ([^\[\]]*?) \s* \])+ )
95: ) !mgxn) {
96: $sub{$+{sub}} = {%+} if exists $+{sub} and "_" ne substr $+{sub}, 0, 1;
97: $has{$+{has}} = {%+} if exists $+{has} and "_" ne substr $+{has}, 0, 1;
98: }
99:
100: return $self if !keys %sub && !keys %has;
101:
102: $_ = read_text $md;
103:
104: my $pkg = _pkg $md;
105:
106: my $added = 0;
107:
108: s{^\#[\ \t]+( (?<is>METHODS|SUBROUTINES) | DESCRIPTION )
109: (^```.*?^```|.)*? (?= ^\#\s)
110: }{
111: my $x = $&; my $is = $+{is};
112: if($is) {
113: while($x =~ /^\#\#[\ \t]+(\w+)/gm) {
114: delete $sub{$1};
115: }
116: }
117: $added += keys %sub;
118: join "", $x, $is? (): "# SUBROUTINES/METHODS\n\n", map { _md_method $pkg, $_, $sub{$_}{args}, $sub{$_}{remark} } sort keys %sub;
119: }emsx or die "Нет секции DESCRIPTION!" if keys %sub;
120:
121: s{^\#[\ \t]+((?<is>FEATURES) | DESCRIPTION)
122: (^```.*?^```|.)*? (?= ^\#\s)
123: }{
124: my $x = $&; my $is = $+{is};
125: if($is) {
126: while($x =~ /^\#\#[\ \t]+([^\n]+?)[\ \t]*/gm) {
127: delete $has{$1};
128: }
129: }
130: $added += keys %has;
131: join "", $x, $is? (): "# FEATURES\n\n", map { _md_feature $pkg, $_, $sub{$_}{remark} } sort keys %has;
132: }emsx or die "Нет секции DESCRIPTION!" if keys %has;
133:
134:
135: if ($added) {
136: write_text $md, $_;
137: print "🔖 $pm ", colored("⊂", "BRIGHT_GREEN"), " $md ", "\n",
138: " ", scalar keys %has? (colored("FEATURES ", "BRIGHT_WHITE"), join(colored(", ", "red"), sort keys %has), "\n"): (),
139: " ", scalar keys %sub? (colored("SUBROUTINES ", "BRIGHT_WHITE"), join(colored(", ", "red"), sort keys %sub), "\n"): (),
140: ;
141: } else {
142: print "🔖 $pm\n";
143: }
144:
145: $self->{count}++;
146: $self->{added} = $added;
147: $self
148: }
149:
150: sub _git_user_name { shift->{_git_user_name} //= _trim(`git config user.name`) }
151: sub _git_user_email { shift->{_git_user_email} //= _trim(`git config user.email`) }
152: sub _year { shift->{_year} //= _trim(`date +%Y`) }
153: sub _license { shift->{_license} //= -r "minil.toml" && read_text("minil.toml") =~ /^\s*license\s*=\s*"([^"\n]*)"/m ? ($1 eq "perl_5"? "Perl5": uc($1) =~ s/_/v/r): "Perl5" }
154: sub _land { shift->{_land} //= `curl "https://ipapi.co/\$(curl https://2ip.ru --connect-timeout 3 --max-time 3 -Ss)/json/" --connect-timeout 3 --max-time 3 -Ss` =~ /country_name": "([^"\n]*)"/ ? ($1 eq "Russia" ? "Rusland" : $1) : 'Rusland' }
155:
156: # Добавить разделы функций в *.md из *.pm
157: sub mkmd {
158: my ($self, $md) = @_;
159:
160: my $pkg = _pkg $md;
161:
162: my $author = $self->_git_user_name;
163: my $email = $self->_git_user_email;
164: my $year = $self->_year;
165: my $license = $self->_license;
166: my $land = $self->_land;
167:
168: write_text $md, << "END";
169: # NAME
170:
171: $pkg -
172:
173: # SYNOPSIS
174:
175: ```perl
176: use $pkg;
177:
178: my ${\_var $pkg} = $pkg->new;
179: ```
180:
181: # DESCRIPION
182:
183: .
184:
185: # SUBROUTINES
186:
187: # INSTALL
188:
189: For install this module in your system run next [command](https://metacpan.org/pod/App::cpm):
190:
191: ```sh
192: sudo cpm install -gvv $pkg
193: ```
194:
195: # AUTHOR
196:
197: $author [$email](mailto:$email)
198:
199: # LICENSE
200:
201: ⚖ **$license**
202:
203: # COPYRIGHT
204:
205: The $pkg module is copyright © $year $author. $land. All rights reserved.
206: END
207: }
208:
209: # Получить путь к тестовому файлу из пути к md-файлу
210: sub test_path {
211: my ($self, $md) = @_;
212: $md =~ s!^lib/(.*)\.md$!
213: join "", "t/", join("/", map {
214: lcfirst($_) =~ s/[A-Z]/"-" . lc $&/gre
215: } split /\//, $1), ".t"
216: !e;
217: $md
218: }
219:
220: # Трансформирует md-файлы
221: sub transforms {
222: my ($self) = @_;
223: my $mds = $self->{files} // [split /\n/, `find lib -name '*.md'`];
224:
225: $self->{count} = 0;
226:
227: if($self->{compile_force}) {
228: $self->transform($_) for @$mds;
229: } else {
230: for my $md (@$mds) {
231: my $test = $self->test_path($md);
232: my $mdmtime = (stat $md)[9];
233: die "Нет файла $md" if !$mdmtime;
234: $self->transform($md, $test) if !-e $test || -e $test && $mdmtime > (stat $test)[9];
235: }
236: }
237:
238: if(-f "minil.toml" && -r "minil.toml") {
239: my $is_copy; my $name;
240: eval {
241: my $minil = read_text("minil.toml");
242: ($name) = $minil =~ /^name = "([\w:-]+)"/m;
243: $name =~ s!(-|::)!/!g;
244: $name = "lib/$name.md";
245: if(-f $name && -r $name) {
246: if(!-e "README.md" || -e "README.md"
247: && (stat $name)[9] > (stat "README.md")[9]) {
248: write_text "README.md", read_text $name;
249: $is_copy = 1;
250: }
251: }
252: };
253: if($@) {warn $@}
254: elsif($is_copy) {
255: print "📘 $name ", colored("↦", "white"), " README.md ", colored("...", "white"), " ", colored("ok", "bright_green"), "\n";
256: }
257: }
258:
259: $self
260: }
261:
262: # Эскейпинг для qr!!
263: sub _qr_esc {
264: $_[0] =~ s/!/\\!/gr
265: }
266:
267: # Эскейпинг для строки в двойных кавычках
268: sub _qq_esc {
269: $_[0] =~ s!"!\\"!gr
270: }
271:
272: # Эскейпинг для строки в одинарных кавычках
273: sub _q_esc {
274: $_[0] =~ s!'!\\'!gr
275: }
276:
277: # Обрезает пробельные символы
278: sub _trim {
279: $_[0] =~ s!^\s*(.*?)\s*\z!$1!sr
280: }
281:
282: # Создаёт путь
283: sub _mkpath {
284: my ($p) = @_;
285: mkdir $`, 0755 while $p =~ /\//g;
286: }
287:
288: # Строка кода для тестирования
289: sub _to_testing {
290: my ($line, %x) = @_;
291:
292: return $x{code} if $x{code} =~ /^\s*#/;
293:
294: my $expected = $x{expected};
295: my $q = _q_esc($line =~ s!\s*$!!r);
296: my $code = _trim($x{code});
297:
298: if(exists $x{is_deeply}) { "::is_deeply scalar do {$code}, scalar do {$expected}, '$q';\n" }
299: elsif(exists $x{is}) { "::is scalar do {$code}, scalar do{$expected}, '$q';\n" }
300: elsif(exists $x{qqis}) { my $ex = _qq_esc($expected); "::is scalar do {$code}, \"$ex\", '$q';\n" }
301: elsif(exists $x{qis}) { my $ex = _q_esc($expected); "::is scalar do {$code}, '$ex', '$q';\n" }
302: elsif(exists $x{like}) { my $ex = _qr_esc($expected); "::like scalar do {$code}, qr!$ex!, '$q';\n" }
303: elsif(exists $x{unlike}) { my $ex = _qr_esc($expected); "::unlike scalar do {$code}, qr!$ex!, '$q';\n" }
304: else { # Что-то ужасное вырвалось на волю!
305: "???"
306: }
307: }
308:
309: # Трансформирует md-файл в тест и документацию
310: sub transform {
311: my ($self, $md, $test) = @_;
312: $test //= $self->test_path($md);
313:
314: print "🔖 $md ", colored("↦", "white"), " $test ", colored("...", "white"), " ";
315:
316: my $markdown = read_text($md);
317:
318: my @pod; my @test; my $title = 'Start'; my $close_subtest; my $use_title = 1;
319:
320: my @text = split /^(```\w*[ \t]*(?:\n|\z))/mo, $markdown;
321:
322: for(my $i=0; $i<@text; $i+=4) {
323: my ($mark, $sec1, $code, $sec2) = @text[$i..$i+4];
324:
325: push @pod, markdown_to_pod($mark);
326: push @test, $mark =~ s/^/# /rmg;
327:
328: last unless defined $sec1;
329: $i--, $sec2 = $code, $code = "" if $code =~ /^```[ \t]*$/;
330:
331: die "=== mark ===\n$mark\n=== sec1 ===\n$sec1\n=== code ===\n$code\n=== sec2 ===\n$sec2\n\nsec2 ne ```" if $sec2 ne "```\n";
332:
333: $title = _trim($1) while $mark =~ /^#+[ \t]+(.*)/gm;
334:
335: push @pod, "\n", ($code =~ s/^/\t/gmr), "\n";
336:
337: my ($infile, $is) = $mark =~ /^(?:File|Файл)[ \t]+(.*?)([\t ]+(?:is|является))?:[\t ]*\n\z/m;
338: if($infile) {
339: my $real_code = $code =~ s/^\\(```\w*[\t ]*$)/$1/mgro;
340: if($is) { # тестируем, что текст совпадает
341: push @test, "\n{ my \$s = '${\_q_esc($infile)}'; open my \$__f__, '<:utf8', \$s or die \"Read \$s: \$!\"; my \$n = join '', <\$__f__>; close \$__f__; ::is \$n, '${\_q_esc($real_code)}', \"File \$s\"; }\n";
342: }
343: else { # записываем тект в файл
344: #push @test, "\n{ my \$s = main::_mkpath_('${\_q_esc($infile)}'); open my \$__f__, '>:utf8', \$s or die \"Read \$s: \$!\"; print \$__f__ '${\_q_esc($real_code)}'; close \$__f__ }\n";
345: push @test, "#\@> $infile\n", $real_code =~ s/^/#>> /rgm, "#\@< EOF\n";
346: }
347: } elsif($sec1 =~ /^```(?:perl)?[ \t]*$/) {
348:
349: if($use_title ne $title) {
350: push @test, "done_testing; }; " if $close_subtest;
351: $close_subtest = 1;
352: push @test, "subtest '${\ _q_esc($title)}' => sub { ";
353: $use_title = $title;
354: }
355:
356: my $test = $code =~ s{^(?<code>.*)#[ \t]*((?<is_deeply>-->|⟶)|(?<is>->|→)|(?<qqis>=>|⇒)|(?<qis>\\>|↦)|(?<like>~>|↬)|(?<unlike><~|↫))\s*(?<expected>.+?)[ \t]*\n}{ _to_testing($&, %+) }grme;
357: push @test, "\n", $test, "\n";
358: }
359: else {
360: push @test, "\n", $code =~ s/^/# /rmg, "\n";
361: }
362: }
363:
364: push @test, "\n\tdone_testing;\n};\n" if $close_subtest;
365: push @test, "\ndone_testing;\n";
366:
367: _mkpath($test);
368: my $mkpath = q{sub _mkpath_ { my ($p) = @_; length($`) && !-e $`? mkdir($`, 0755) || die "mkdir $`: $!": () while $p =~ m!/!g; $p }};
369: my $write_files = q{open my $__f__, "<:utf8", $t or die "Read $t: $!"; read $__f__, $s, -s $__f__; close $__f__; while($s =~ /^#\\@> (.*)\n((#>> .*\n)*)#\\@< EOF\n/gm) { my ($file, $code) = ($1, $2); $code =~ s/^#>> //mg; open my $__f__, ">:utf8", _mkpath_($file) or die "Write $file: $!"; print $__f__ $code; close $__f__; }};
370: #my @symbol = ('a'..'z', 'A'..'Z', '0' .. '9', '-', '_');
371: # "-" . join("", map $symbol[rand(scalar @symbol)], 1..6)
372: my $test_path = join "", "/tmp/.liveman/",
373: `pwd` =~ s/^.*?([^\/]+)\n$/$1/rs,
374: $test =~ s!^t/(.*)\.t$!/$1!r =~ y/\//!/r, "/";
375: my $chdir = "my \$t = `pwd`; chop \$t; \$t .= '/' . __FILE__; my \$s = '${\ _q_esc($test_path)}'; `rm -fr '\$s'` if -e \$s; chdir _mkpath_(\$s) or die \"chdir \$s: \$!\";";
376: # use Carp::Always::Color ::Term;
377: my $die = 'use Scalar::Util qw//; use Carp qw//; $SIG{__DIE__} = sub { my ($s) = @_; if(ref $s) { $s->{STACKTRACE} = Carp::longmess "?" if "HASH" eq Scalar::Util::reftype $s; die $s } else {die Carp::longmess defined($s)? $s: "undef" }};';
378: write_text $test, join "", "use common::sense; use open qw/:std :utf8/; use Test::More 0.98; $mkpath BEGIN { $die $chdir $write_files } ", @test;
379:
380: # Создаём модуль, если его нет
381: my $pm = $md =~ s/\.md$/.pm/r;
382: if(!-e $pm) {
383: my $pkg = ($pm =~ s!^lib/(.*)\.pm$!$1!r) =~ s!/!::!gr;
384: write_text $pm, "package $pkg;\n\n1;";
385: }
386:
387: # Трансформируем модуль (pod и версия):
388: my $pod = join "", @pod;
389: my $module = read_text $pm;
390: $module =~ s!(\s*\n__END__[\t ]*\n.*)?$!\n\n__END__\n\n=encoding utf-8\n\n$pod!sn;
391:
392: # Меняем версию:
393: my $v = uc "version";
394: my ($version) = $markdown =~ /^#[ \t]+$v\s+([\w\.-]{1,32})\s/m;
395: $module =~ s!^(our\s*\$$v\s*=\s*)["']?[\w.-]{1,32}["']?!$1"$version"!m if defined $version;
396: write_text $pm, $module;
397:
398: $self->{count}++;
399:
400: print colored("ok", "bright_green"), "\n";
401:
402: $self
403: }
404:
405: # Запустить тесты
406: sub tests {
407: my ($self) = @_;
408:
409: my $cover = "/usr/bin/site_perl/cover";
410: $cover = 'cover' if !-e $cover;
411:
412: my $yath = "/usr/bin/site_perl/yath";
413: $yath = 'yath' if !-e $yath;
414:
415: my $options = $self->{options};
416:
417: if($self->{files}) {
418: my @tests = map $self->test_path($_), @{$self->{files}};
419: local $, = " ";
420: $self->{exit_code} = system $self->{prove}
421: ? "prove -Ilib $options @tests"
422: : "$yath test -j4 $options @tests";
423: return $self;
424: }
425:
426: my $perl5opt = $ENV{PERL5OPT};
427:
428: system "$cover -delete";
429: if($self->{prove}) {
430: local $ENV{PERL5OPT} = "$perl5opt -MDevel::Cover";
431: $self->{exit_code} = system "env | grep PERL5OPT; prove -Ilib -r t $options";
432: #$self->{exit_code} = system "prove --exec 'echo `pwd`/lib && perl -MDevel::Cover -I`pwd`/lib' -r t";
433: } else {
434: $self->{exit_code} = system "$yath test -j4 --cover $options";
435: }
436: return $self if $self->{exit_code};
437: system "$cover -report html_basic";
438: system "(opera cover_db/coverage.html || xdg-open cover_db/coverage.html) &> /dev/null" if $self->{open};
439: return $self;
440: }
441:
442: 1;
443:
444: __END__
445:
446: =encoding utf-8
447:
448: =head1 NAME
449:
450: Liveman - markdown compiller to test and pod.
451:
452: =head1 VERSION
453:
454: 1.0
455:
456: =head1 SYNOPSIS
457:
458: File lib/Example.md:
459:
460: Twice two:
461: \```perl
462: 2*2 # -> 2+2
463: \```
464:
465: Test:
466:
467: use Liveman;
468:
469: my $liveman = Liveman->new(prove => 1);
470:
471: # compile lib/Example.md file to t/example.t and added pod to lib/Example.pm
472: $liveman->transform("lib/Example.md");
473:
474: $liveman->{count} # => 1
475: -f "t/example.t" # => 1
476: -f "lib/Example.pm" # => 1
477:
478: # compile all lib/**.md files with a modification time longer than their corresponding test files (t/**.t)
479: $liveman->transforms;
480: $liveman->{count} # => 0
481:
482: # compile without check modification time
483: Liveman->new(compile_force => 1)->transforms->{count} # => 1
484:
485: # start tests with yath
486: my $yath_return_code = $liveman->tests->{exit_code};
487:
488: $yath_return_code # => 0
489: -f "cover_db/coverage.html" # => 1
490:
491: # limit liveman to these files for operations transforms and tests (without cover)
492: my $liveman2 = Liveman->new(files => [], force_compile => 1);
493:
494: =head1 DESCRIPION
495:
496: The problem with modern projects is that the documentation is disconnected from testing.
497: This means that the examples in the documentation may not work, and the documentation itself may lag behind the code.
498:
499: Liveman compile C<lib/**>.md files to C<t/**.t> files
500: and it added pod-documentation to section C<__END__> to C<lib/**.pm> files.
501:
502: Use C<liveman> command for compile the documentation to the tests in catalog of your project and starts the tests:
503:
504: liveman
505:
506: Run it with coverage.
507:
508: Option C<-o> open coverage in browser (coverage file: C<cover_db/coverage.html>).
509:
510: Liveman replace C<our $VERSION = "...";> in C<lib/**.pm> from C<lib/**.md> if it exists in pm and in md.
511:
512: If exists file B<minil.toml>, then Liveman read C<name> from it, and copy file with this name and extension C<.md> to README.md.
513:
514: =head2 TYPES OF TESTS
515:
516: Section codes C<noname> or C<perl> writes as code to C<t/**.t>-file. And comment with arrow translates on test from module C<Test::More>.
517:
518: The test name set as the code-line.
519:
520: =head3 C<is>
521:
522: Compare two expressions for equivalence:
523:
524: "hi!" # -> "hi" . "!"
525: "hi!" # → "hi" . "!"
526:
527: =head3 C<is_deeply>
528:
529: Compare two expressions for structures:
530:
531: "hi!" # --> "hi" . "!"
532: "hi!" # ⟶ "hi" . "!"
533:
534: =head3 C<is> with extrapolate-string
535:
536: Compare expression with extrapolate-string:
537:
538: my $exclamation = "!";
539: "hi!2" # => hi${exclamation}2
540: "hi!2" # ⇒ hi${exclamation}2
541:
542: =head3 C<is> with nonextrapolate-string
543:
544: Compare expression with nonextrapolate-string:
545:
546: 'hi${exclamation}3' # \> hi${exclamation}3
547: 'hi${exclamation}3' # ↦ hi${exclamation}3
548:
549: =head3 C<like>
550:
551: It check a regular expression included in the expression:
552:
553: 'abbc' # ~> b+
554: 'abc' # ↬ b+
555:
556: =head3 C<unlike>
557:
558: It check a regular expression excluded in the expression:
559:
560: 'ac' # <~ b+
561: 'ac' # ↫ b+
562:
563: =head2 EMBEDDING FILES
564:
565: Each test is executed in a temporary directory, which is erased and created when the test is run.
566:
567: This directory format is /tmp/.liveman/I<project>/I<path-to-test>/.
568:
569: Code section in md-file prefixed line B<< File C<path>: >> write to file in rintime testing.
570:
571: Code section in md-file prefixed line B<< File C<path> is: >> will be compared with the file by the method C<Test::More::is>.
572:
573: File experiment/test.txt:
574:
575: hi!
576:
577: File experiment/test.txt is:
578:
579: hi!
580:
581: B<Attention!> An empty string between the prefix and the code is not allowed!
582:
583: Prefixes maybe on russan: C<Файл path:> and C<Файл path является:>.
584:
585: =head1 METHODS
586:
587: =head2 new (%param)
588:
589: Constructor. Has arguments:
590:
591: =over
592:
593: =item 1. C<files> (array_ref) — list of md-files for methods C<transforms> and C<tests>.
594:
595: =item 2. C<open> (boolean) — open coverage in browser. If is B<opera> browser — open in it. Else — open via C<xdg-open>.
596:
597: =item 3. C<force_compile> (boolean) — do not check the md-files modification time.
598:
599: =item 4. C<options> — add options in command line to yath or prove.
600:
601: =item 5. C<prove> — use prove, but use'nt yath.
602:
603: =back
604:
605: =head2 test_path ($md_path)
606:
607: Get the path to the C<t/**.t>-file from the path to the C<lib/**.md>-file:
608:
609: Liveman->new->test_path("lib/PathFix/RestFix.md") # => t/path-fix/rest-fix.t
610:
611: =head2 transform ($md_path, [$test_path])
612:
613: Compile C<lib/**.md>-file to C<t/**.t>-file.
614:
615: And method C<transform> replace the B<pod>-documentation in section C<__END__> in C<lib/**.pm>-file. And create C<lib/**.pm>-file if it not exists.
616:
617: File lib/Example.pm is:
618:
619: package Example;
620:
621: 1;
622:
623: __END__
624:
625: =encoding utf-8
626:
627: Twice two:
628:
629: 2*2 # -> 2+2
630:
631:
632: File C<lib/Example.pm> was created from file C<lib/Example.md> described in section C<SINOPSIS> in this document.
633:
634: =head2 transforms ()
635:
636: Compile C<lib/**.md>-files to C<t/**.t>-files.
637:
638: All if C<< $self-E<gt>{files} >> is empty, or C<< $self-E<gt>{files} >>.
639:
640: =head2 tests ()
641:
642: Tests C<t/**.t>-files.
643:
644: All if C<< $self-E<gt>{files} >> is empty, or C<< $self-E<gt>{files} >> only.
645:
646: =head2 mkmd ($md)
647:
648: It make md-file.
649:
650: =head2 appends ()
651:
652: Append to C<lib/**.md> from C<lib/**.pm> subroutines and features.
653:
654: =head2 append ($path)
655:
656: Append subroutines and features from the module with C<$path> into its documentation in the its sections.
657:
658: File lib/Alt/The/Plan.pm:
659:
660: package Alt::The::Plan;
661:
662: sub planner {
663: my ($self) = @_;
664: }
665:
666: # This is first!
667: sub miting {
668: my ($self, $meet, $man, $woman) = @_;
669: }
670:
671: sub _exquise_me {
672: my ($self, $meet, $man, $woman) = @_;
673: }
674:
675: 1;
676:
677:
678:
679: -e "lib/Alt/The/Plan.md" # -> undef
680:
681: # Set the mocks:
682: *Liveman::_git_user_name = sub {'Yaroslav O. Kosmina'};
683: *Liveman::_git_user_email = sub {'dart@cpan.org'};
684: *Liveman::_year = sub {2023};
685: *Liveman::_license = sub {"Perl5"};
686: *Liveman::_land = sub {"Rusland"};
687:
688: my $liveman = Liveman->new->append("lib/Alt/The/Plan.pm");
689: $liveman->{count} # -> 1
690: $liveman->{added} # -> 2
691:
692: -e "lib/Alt/The/Plan.md" # -> 1
693:
694: # And again:
695: $liveman = Liveman->new->append("lib/Alt/The/Plan.pm");
696: $liveman->{count} # -> 1
697: $liveman->{added} # -> 0
698:
699: File lib/Alt/The/Plan.md is:
700:
701: # NAME
702:
703: Alt::The::Plan -
704:
705: # SYNOPSIS
706:
707: \```perl
708: use Alt::The::Plan;
709:
710: my $alt_the_plan = Alt::The::Plan->new;
711: \```
712:
713: # DESCRIPION
714:
715: .
716:
717: # SUBROUTINES
718:
719: ## miting ($meet, $man, $woman)
720:
721: This is first!
722:
723: \```perl
724: my $alt_the_plan = Alt::The::Plan->new;
725: $alt_the_plan->miting($meet, $man, $woman) # -> .3
726: \```
727:
728: ## planner ()
729:
730: .
731:
732: \```perl
733: my $alt_the_plan = Alt::The::Plan->new;
734: $alt_the_plan->planner # -> .3
735: \```
736:
737: # INSTALL
738:
739: For install this module in your system run next [command](https://metacpan.org/pod/App::cpm):
740:
741: \```sh
742: sudo cpm install -gvv Alt::The::Plan
743: \```
744:
745: # AUTHOR
746:
747: Yaroslav O. Kosmina [dart@cpan.org](mailto:dart@cpan.org)
748:
749: # LICENSE
750:
751: ⚖ **Perl5**
752:
753: # COPYRIGHT
754:
755: The Alt::The::Plan module is copyright © 2023 Yaroslav O. Kosmina. Rusland. All rights reserved.
756:
757: =head1 INSTALL
758:
759: Add to B<cpanfile> in your project:
760:
761: on 'test' => sub {
762: requires 'Liveman',
763: git => 'https://github.com/darviarush/perl-liveman.git',
764: ref => 'master',
765: ;
766: };
767:
768: And run command:
769:
770: $ sudo cpm install -gvv
771:
772: =head1 AUTHOR
773:
774: Yaroslav O. Kosmina LL<mailto:dart@cpan.org>
775:
776: =head1 LICENSE
777:
778: ⚖ B<GPLv3>
779:
780: =head1 COPYRIGHT
781:
782: The Alt::The::Plan module is copyright © 2023 Yaroslav O. Kosmina. Rusland. All rights reserved.
783: