view release on metacpan or search on metacpan
0.14 2023-12-13
[CHANGES]
* Handle non-breaking spaces in Markdown and Pod
* Updated for Object::Pad v0.807:
+ Use new `inherit` and `apply` keywords
0.13 2023-09-22
[CHANGES]
* Allow overriding of inline format styles in App::sdview::Style
config file
* Use format-agnostic tag names "bold", "italic", etc.. rather than
POD-inspired "B", "I", etc..
* Support Markdown's ~~strikethrough~~ format
* Preserve the language name in Markdown code fences
* Recognise and emit U<underline> formatting as a POD extension
0.12 2023-08-30
[CHANGES]
* Neater interaction between `margin` and `indent` style keys
* User-overridable style formatting by providing a `~/.sdviewrc` file
lib/App/sdview/Output/Formatted.pm view on Meta::CPAN
:$leader = undef,
:$indent //= 0,
) {
my %typestyle = App::sdview::Style->para_style( $para->type )->%*;
$self->say() if $_nextblank;
my $text = App::sdview::Style->convert_str( $para->text );
$typestyle{$_} and $text->apply_tag( 0, -1, $_ => $typestyle{$_} )
for qw( fg bg bold under italic monospace );
$_nextblank = !!$typestyle{blank_after};
my @lines = $text->split( qr/\n/ );
@lines or @lines = ( String::Tagged->new ) if defined $leader;
# If there's a background set, then space-pad every line to the same width
# so it looks neater on the terminal
# https://rt.cpan.org/Ticket/Display.html?id=140536
if( defined $typestyle{bg} ) {
lib/App/sdview/Output/Formatted.pm view on Meta::CPAN
else {
$part = $line;
$line = "";
}
my $prefix = " "x$margin;;
if( defined $leader ) {
my %leaderstyle = App::sdview::Style->para_style( "leader" )->%*;
$leaderstyle{$_} and $leader->apply_tag( 0, -1, $_ => $leaderstyle{$_} )
for qw( fg bg bold under italic monospace );
if( length($leader) + 1 <= $indent ) {
# If the leader will fit on the same line with at least one space
$prefix .= $leader . " "x($indent - length $leader);
}
else {
# Spill the leader onto its own line
$self->say( $prefix, $leader );
$prefix .= " "x$indent if length $part;
lib/App/sdview/Output/Formatted.pm view on Meta::CPAN
foreach my $colidx ( 0 .. $maxcol ) {
my $cell = $row->[$colidx];
my $text = App::sdview::Style->convert_str( $cell->text );
my %cellstyle = %typestyle;
%cellstyle = ( App::sdview::Style->para_style( "table-heading" )->%*, %cellstyle ) if $cell->heading;
$cellstyle{$_} and $text->apply_tag( 0, -1, $_ => $cellstyle{$_} )
for qw( fg bg bold under italic monospace );
my $spare = $colwidths[$colidx] - length $text;
my $leftpad = ( $cell->align eq "right" ) ? " "x$spare :
( $cell->align eq "centre" ) ? " "x($spare/2) :
"";
my $rightpad = " "x($spare - length $leftpad);
$out .= " " . $leftpad . $text . $rightpad . " ";
$out .= "â";
}
lib/App/sdview/Output/Man.pm view on Meta::CPAN
my @fontstack;
$s->iter_substr_nooverlap(
sub ( $substr, %tags ) {
$ret .= "\\fP", pop @fontstack
while @fontstack and !$tags{ $fontstack[-1] };
$tags{monospace} and (
any { $_ eq "monospace" } @fontstack or
$ret .= "\\f(CW", push @fontstack, "monospace" );
$tags{bold} and (
any { $_ eq "bold" } @fontstack or
$ret .= "\\fB", push @fontstack, "bold" );
$tags{italic} and (
any { $_ eq "italic" } @fontstack or
$ret .= "\\fI", push @fontstack, "italic" );
my $man = $substr =~ s/([\\-])/\\$1/gr;
$ret .= $man;
}
);
lib/App/sdview/Output/Markdown.pm view on Meta::CPAN
("-"x $n );
} @cells;
$self->say( join "|", "", ( map { " $_ " } @aligns ), "" );
undef $first;
}
}
method _convert_str ( $s )
{
return String::Tagged::Markdown->clone( $s,
only_tags => [qw( bold italic monospace strikethrough file link )],
convert_tags => {
# bold, italic remain as they are
monospace => "fixed",
strikethrough => "strike",
file => "italic", # There isn't a "filename" format in Markdown
link => sub ($t, $v) { return link => $v->{uri} },
}
)->build_markdown;
}
=head1 AUTHOR
lib/App/sdview/Output/Pod.pm view on Meta::CPAN
# TODO: This is even suckier than the bit in the parser
if( $link->{uri} eq "https://metacpan.org/pod/$substr" ) {
$pod = "L$open$substr$close";
}
else {
$pod = "L$open$pod|$link->{uri}$close";
}
}
$pod = "C$open$pod$close" if $tags{monospace};
$pod = "B$open$pod$close" if $tags{bold};
$pod = "I$open$pod$close" if $tags{italic};
$pod = "F$open$pod$close" if $tags{file};
$pod = "U$open$pod$close" if $tags{underline};
$pod = "S$open$pod$close" if $tags{nobreak};
$ret .= $pod;
}
);
lib/App/sdview/Parser/Man.pm view on Meta::CPAN
}
else {
print STDERR "TODO: para->type = $type\n";
}
}
return @_paragraphs;
}
my %FONTTAGS = (
B => { bold => 1 },
I => { italic => 1 },
CW => { monospace => 1 },
);
sub _chunklist_to_taggedstring ( $chunks, :$linefeed = " " )
{
my $ret = String::Tagged->new;
foreach my $chunk ( $chunks->@* ) {
my %tags;
lib/App/sdview/Parser/Markdown.pm view on Meta::CPAN
=head1 DESCRIPTION
This parser module adds to L<App::sdview> the ability to parse input text in
Markdown formatting.
It uses a custom in-built parser for the block-level parts of the formatting,
able to handle comments, verbatim blocks, headings in both C<#>-prefixed and
C<=>-underlined styles, bullet and numbered lists, and tables.
It uses L<String::Tagged::Markdown> to parse the inline-level formatting,
supporting bold, italic, strikethrough, and fixed-width styles, and links.
=cut
sub find_file ( $class, $name ) { return undef }
sub can_parse_file ( $class, $file )
{
return $file =~ m/\.(?:md|markdown)$/;
}
lib/App/sdview/Parser/Markdown.pm view on Meta::CPAN
}
return @_paragraphs;
}
method _handle_spans ( $s )
{
return String::Tagged::Markdown->parse_markdown( $s )
->clone(
convert_tags => {
# bold, italic stay as they are
fixed => "monospace",
strike => "strikethrough",
link => sub ($t, $v) { return link => { uri => $v } },
},
only_tags => [qw( bold italic fixed strike link )],
);
}
=head1 AUTHOR
Paul Evans <leonerd@leonerd.org.uk>
=cut
0x55AA;
lib/App/sdview/Parser/Pod.pm view on Meta::CPAN
role App::sdview::Parser::Pod::_TagHandler {
ADJUST {
$self->nix_X_codes( 1 );
$self->accept_codes(qw( U ));
}
field %_curtags :reader;
method reset_tags { %_curtags = (); }
method start_B { $_curtags{bold}++ }
method end_B { delete $_curtags{bold} }
method start_I { $_curtags{italic}++ }
method end_I { delete $_curtags{italic} }
method start_U { $_curtags{underline}++ }
method end_U { delete $_curtags{underline} }
method start_C { $_curtags{monospace}++ }
method end_C { delete $_curtags{monospace} }
method start_F { $_curtags{file}++ }
method end_F { delete $_curtags{file} }
method start_L ( $attrs )
lib/App/sdview/Style.pm view on Meta::CPAN
=head2 Config File
=for highlighter
Style information can be overridden by the user, supplying a
L<Config::Tiny>-style file at F<$HOME/.sdviewrc>. Formatting for each kind of
paragraph is provided in a section called C<Para $NAME>, and each individual
key gives formatting values.
[Para head1]
bold = 0|1
italic = 0|1
monospace = 0|1
blank_after = 0|1
under = NUM
margin = NUM
[Para head2]
...
Specifying the special value C<~> deletes the default value for that key
lib/App/sdview/Style.pm view on Meta::CPAN
=cut
sub _fixup_colour_keys ( $style )
{
$style->{$_} and
$style->{$_} = Convert::Color->new( $style->{$_} ) for qw( fg bg );
}
my %FORMATSTYLES = (
bold => { bold => 1 },
italic => { italic => 1 },
monospace => { monospace => 1, bg => "xterm:235" },
underline => { under => 1 },
strikethrough => { strike => 1 },
file => { italic => 1, under => 1 },
link => { under => 1, fg => "xterm:rgb(3,3,5)" }, # light blue
);
_fixup_colour_keys $_ for values %FORMATSTYLES;
lib/App/sdview/Style.pm view on Meta::CPAN
}
else {
$k => sub { $FORMATSTYLES{$k}->%* };
}
} keys %FORMATSTYLES ),
},
);
}
my %PARASTYLES = (
head1 => { fg => "vga:yellow", bold => 1 },
head2 => { fg => "vga:cyan", bold => 1, margin => 2 },
head3 => { fg => "vga:green", bold => 1, margin => 4 },
head4 => { fg => "xterm:217", under => 1, margin => 5 },
plain => { margin => 6, blank_after => 1 },
verbatim => { margin => 8, blank_after => 1, inherit => "monospace" },
list => { margin => 6 },
item => { blank_after => 1 },
leader => { bold => 1 },
table => { margin => 8 },
"table-heading" => { bold => 1 },
);
_fixup_colour_keys $_ for values %PARASTYLES;
sub para_style ( $pkg, $type )
{
$PARASTYLES{$type} or
die "Unrecognised paragraph style for $type";
my %style = $PARASTYLES{$type}->%*;
%style = ( %style, $FORMATSTYLES{delete $style{inherit}}->%* ) if defined $style{inherit};
lib/App/sdview/Style.pm view on Meta::CPAN
return \%style;
}
my %HIGHLIGHTSTYLES = (
# Names stolen from tree-sitter's highlight theme
attribute => { fg => "vga:cyan", italic => 1 },
character => { fg => "vga:magenta" },
comment => { fg => "xterm:15", bg => "xterm:54", italic => 1 },
decorator => { fg => "xterm:140", italic => 1 },
function => { fg => "xterm:147", },
keyword => { fg => "vga:yellow", bold => 1 },
module => { fg => "vga:green", bold => 1 },
number => { fg => "vga:magenta" },
operator => { fg => "vga:yellow" },
string => { fg => "vga:magenta" },
type => { fg => "vga:green" },
variable => { fg => "vga:cyan" },
'string.special' => { fg => "vga:red" },
'function.builtin' => { fg => "xterm:147", bold => 1 },
);
$HIGHLIGHTSTYLES{$_} = { fallback => "keyword" } for qw( include repeat conditional exception );
$HIGHLIGHTSTYLES{$_} = { fallback => "function" } for qw( method );
_fixup_colour_keys $_ for values %HIGHLIGHTSTYLES;
sub highlight_style ( $pkg, $key )
{
my @nameparts = split m/\./, $key;
while( @nameparts ) {
my $style = $HIGHLIGHTSTYLES{ join ".", @nameparts } or
lib/App/sdview/Style.pm view on Meta::CPAN
}
return $style;
}
return undef;
}
my %VALID_STYLE_KEYS = map { $_ => 1 } qw(
fg bg
bold italic monospace blank_after
under margin
);
sub _convert_val ( $stylekey, $val )
{
return undef if !defined $val or $val eq "~";
if( $stylekey =~ m/^(fg|bg)$/ ) {
return Convert::Color->new( $val );
}
elsif( $stylekey =~ m/^(bold|italic|monospace|blank_after)$/ ) {
return !!$val;
}
elsif( $stylekey =~ m/^(under|margin)$/ ) {
return 0+$val;
}
else {
return undef;
}
}
t/01style.t view on Meta::CPAN
use Test2::V0;
use App::sdview::Style;
use Convert::Color;
# Default style
{
is( App::sdview::Style->para_style( "head1" ),
{
bold => T(),
fg => Convert::Color->new( "vga:yellow" ),
},
'Default head1 paragraph style' );
is( App::sdview::Style->inline_style( "monospace" ),
{
monospace => T(),
bg => Convert::Color->new( "xterm:235" ),
},
'Default monospace inline style' );
t/01style.t view on Meta::CPAN
},
'Default method highlight style falls back to keyword' );
}
# Load a custom config file
{
App::sdview::Style->load_config( \*DATA );
is( App::sdview::Style->para_style( "head1" ),
{
bold => T(),
fg => Convert::Color->new( "vga:red" ),
},
'Overridden head1 paragraph style' );
is( App::sdview::Style->inline_style( "monospace" ),
{
monospace => T(),
},
'Overridden monospace inline style' );
t/10parser-pod.t view on Meta::CPAN
ok( App::sdview::Parser::Pod->can_parse_file( "Example.pod" ), 'Parser can handle .pod file' );
subtest "Basic" => sub {
my @p = App::sdview::Parser::Pod->new->parse_string( <<"EOPOD" );
=head1 Heading
The heading paragraph here.
=head2 Content
The content with B<bold> and C<code> in it.
EOPOD
is( scalar @p, 4, 'Received 4 paragraphs' );
is( $p[0]->type, "head1", 'p[0] type' );
is( $p[0]->text, "Heading", 'p[0] text' );
is( $p[1]->type, "plain", 'p[1] type' );
is( $p[1]->text, "The heading paragraph here.", 'p[1] text' );
is( $p[2]->type, "head2", 'p[2] type' );
is( $p[2]->text, "Content", 'p[2] text' );
is( $p[3]->type, "plain", 'p[3] type' );
is( $p[3]->text, "The content with bold and code in it.", 'p[3] text' );
is( [ sort $p[3]->text->tagnames ], [qw( bold monospace )], 'p[3] tags' );
};
subtest "Formatting" => sub {
my @p = App::sdview::Parser::Pod->new->parse_string( <<"EOPOD" );
=pod
B<bold> B<< bold >>
I<italic> I<< italic >>
C<code> C<< code->with->arrows >>
F<filename>
L<link|target://> L<Module::Here>
U<underline> U<< underline >>
EOPOD
is( scalar @p, 6, 'Received 6 paragraphs' );
is( $p[0]->text, "bold bold", 'bold text' );
ok( $p[0]->text->get_tag_at( 0, "bold" ), 'bold tag' );
is( $p[1]->text, "italic italic", 'italic text' );
ok( $p[1]->text->get_tag_at( 0, "italic" ), 'italic tag' );
is( $p[2]->text, "code code->with->arrows", 'code text' );
ok( $p[2]->text->get_tag_at( 0, "monospace" ), 'code tag' );
is( $p[3]->text, "filename", 'file text' );
ok( $p[3]->text->get_tag_at( 0, "file" ), 'file tag' );
t/10parser-pod.t view on Meta::CPAN
subtest "Formatted headings" => sub {
my @p = App::sdview::Parser::Pod->new->parse_string( <<"EOPOD" );
=head1 A B<Bold> Beginning
EOPOD
is( scalar @p, 1, 'Received 1 paragraph' );
is( $p[0]->type, "head1", 'p[0] type' );
is( $p[0]->text, "A Bold Beginning", 'p[0] text' );
is( [ sort $p[0]->text->tagnames ], [qw( bold )], 'p[0] tags' );
};
subtest "Non-breaking spaces" => sub {
my @p = App::sdview::Parser::Pod->new->parse_string( <<"EOPOD" );
=pod
Some content with S<non-breaking spaces> in it.
EOPOD
is( scalar @p, 1, 'Received 1 paragraph' );
t/10parser-pod.t view on Meta::CPAN
is( $cells[0]->align, "left", 'col[0] align' );
is( $cells[1]->text, "Centre", 'col[1] text' );
is( $cells[1]->align, "centre", 'col[1] align' );
is( $cells[2]->text, "Right", 'col[2] text' );
is( $cells[2]->align, "right", 'col[2] align' );
@rows = $p[2]->rows;
my @col1 = map { $_->[1] } @rows;
is( $col1[0]->text, "123", 'col1[0] text' );
is( [ $col1[0]->text->tagnames ], [qw( bold )], 'col1[0] text tags' );
ok( !$col1[0]->heading, 'col1[0] heading' );
is( $col1[1]->text, "456", 'col1[1] text' );
is( [ $col1[1]->text->tagnames ], [qw( italic )], 'col1[1] text tags' );
@rows = $p[3]->rows;
is( $rows[0][0]->text, "A1", 'mediawiki cell A1' );
ok( $rows[0][0]->heading, 'mediawiki cell A1 is heading' );
is( $rows[0][1]->text, "A2", 'mediawiki cell A2' );
ok( $rows[0][1]->heading, 'mediawiki cell A2 is heading' );
is( $rows[1][0]->text, "B1", 'mediawiki cell B1' );
t/11parser-markdown.t view on Meta::CPAN
ok( App::sdview::Parser::Markdown->can_parse_file( "Example.md" ), 'Parser can handle .md file' );
subtest "Basic" => sub {
my @p = App::sdview::Parser::Markdown->new->parse_string( <<"EOMARKDOWN" );
# Heading
The heading paragraph here.
## Content
The content with **bold** and `code` in it.
EOMARKDOWN
is( scalar @p, 4, 'Received 4 paragraphs' );
is( $p[0]->type, "head1", 'p[0] type' );
is( $p[0]->text, "Heading", 'p[0] text' );
is( $p[1]->type, "plain", 'p[1] type' );
is( $p[1]->text, "The heading paragraph here.", 'p[1] text' );
is( $p[2]->type, "head2", 'p[2] type' );
is( $p[2]->text, "Content", 'p[2] text' );
is( $p[3]->type, "plain", 'p[3] type' );
is( $p[3]->text, "The content with bold and code in it.", 'p[3] text' );
is( [ sort $p[3]->text->tagnames ], [qw( bold monospace )], 'p[3] tags' );
};
subtest "Alternate headings" => sub {
my @p = App::sdview::Parser::Markdown->new->parse_string( <<"EOMARKDOWN" );
Heading
=======
The heading paragraph here.
Content
-------
The content with **bold** and `code` in it.
EOMARKDOWN
is( scalar @p, 4, 'Received 4 paragraphs' );
is( $p[0]->type, "head1", 'p[0] type' );
is( $p[0]->text, "Heading", 'p[0] text' );
is( $p[1]->type, "plain", 'p[1] type' );
is( $p[1]->text, "The heading paragraph here.", 'p[1] text' );
is( $p[2]->type, "head2", 'p[2] type' );
is( $p[2]->text, "Content", 'p[2] text' );
is( $p[3]->type, "plain", 'p[3] type' );
is( $p[3]->text, "The content with bold and code in it.", 'p[3] text' );
};
subtest "Formatting" => sub {
my @p = App::sdview::Parser::Markdown->new->parse_string( <<"EOMARKDOWN" );
**bold** __bold__
*italic* _italic_
`code` `code_with_unders`
[link](target://)
~~strikethrough~~
EOMARKDOWN
is( scalar @p, 5, 'Received 5 paragraphs' );
is( $p[0]->text, "bold bold", 'bold text' );
ok( $p[0]->text->get_tag_at( 0, "bold" ), 'bold tag' );
is( $p[1]->text, "italic italic", 'italic text' );
ok( $p[1]->text->get_tag_at( 0, "italic" ), 'italic tag' );
is( $p[2]->text, "code code_with_unders", 'code text' );
ok( $p[2]->text->get_tag_at( 0, "monospace" ), 'code tag' );
is( $p[3]->text, "link", 'link text' );
is( $p[3]->text->get_tag_at( 0, "link" ), { uri => "target://" }, 'link tag' );
t/11parser-markdown.t view on Meta::CPAN
is( $cells[0]->align, "left", 'col[0] align' );
is( $cells[1]->text, "Centre", 'col[1] text' );
is( $cells[1]->align, "centre", 'col[1] align' );
is( $cells[2]->text, "Right", 'col[2] text' );
is( $cells[2]->align, "right", 'col[2] align' );
@rows = $p[2]->rows;
my @col1 = map { $_->[1] } @rows;
is( $col1[0]->text, "123", 'col1[0] text' );
is( [ $col1[0]->text->tagnames ], [qw( bold )], 'col1[0] text tags' );
is( $col1[1]->text, "456", 'col1[1] text' );
is( [ $col1[1]->text->tagnames ], [qw( italic )], 'col1[1] text tags' );
};
done_testing;
t/12parser-man.t view on Meta::CPAN
isa_ok( $parser, [ "App::sdview::Parser::Man" ], '$parser' );
ok( App::sdview::Parser::Man->can_parse_file( "Example.3" ), 'Parser can handle .3 file' );
ok( App::sdview::Parser::Man->can_parse_file( "Example.3.gz" ), 'Parser can handle .3.gz file' );
subtest "Basic" => sub {
my @p = App::sdview::Parser::Man->new->parse_string( <<"EOMAN" );
.SH Heading
The heading paragraph here.
.SS Content
The content with \\fBbold\\fP and \\f(CWcode\\fP in it.
EOMAN
is( scalar @p, 4, 'Received 4 paragraphs' );
is( $p[0]->type, "head1", 'p[0] type' );
is( $p[0]->text, "Heading", 'p[0] text' );
is( $p[1]->type, "plain", 'p[1] type' );
is( $p[1]->text, "The heading paragraph here.", 'p[1] text' );
is( $p[2]->type, "head2", 'p[2] type' );
is( $p[2]->text, "Content", 'p[2] text' );
is( $p[3]->type, "plain", 'p[3] type' );
is( $p[3]->text, "The content with bold and code in it.", 'p[3] text' );
is( [ sort $p[3]->text->tagnames ], [qw( bold monospace )], 'p[3] tags' );
};
subtest "Formatting" => sub {
my @p = App::sdview::Parser::Man->new->parse_string( <<"EOMAN" );
.PP
.B bold
\\fBbold\\fP
.PP
.I italic
\\fIitalic\\fP
.PP
\\f(CWcode->with->arrows\\fP
EOMAN
is( scalar @p, 3, 'Received 3 paragraphs' );
is( $p[0]->text, "bold bold", 'bold text' );
ok( $p[0]->text->get_tag_at( 0, "bold" ), 'bold tag' );
is( $p[1]->text, "italic italic", 'italic text' );
ok( $p[1]->text->get_tag_at( 0, "italic" ), 'italic tag' );
is( $p[2]->text, "code->with->arrows", 'code text' );
ok( $p[2]->text->get_tag_at( 0, "monospace" ), 'code tag' );
};
subtest "Verbatim trimming" => sub {
my @p = App::sdview::Parser::Man->new->parse_string( <<"EOMAN" );
t/20output-pod.t view on Meta::CPAN
=head1 Head1
=head2 Head2
Contents here
EOPOD
dotest "Formatting", <<"EOPOD";
=pod
B<bold> B<< <bold> >>
I<italic>
C<code> C<< code->with->arrows >>
F<filename>
L<link|target://> L<Module::Here>
U<underline>
t/21output-markdown.t view on Meta::CPAN
dotest "Headings", <<"EOMARKDOWN";
# Heading
## Content
Contents here
EOMARKDOWN
dotest "Formatting", <<"EOMARKDOWN";
**bold**
*italic*
`code` `code_with_unders`
[link](target://)
~~strikethrough~~
EOMARKDOWN
t/22output-man.t view on Meta::CPAN
}
dotest "Headings", <<"EOMAN";
.SH Head1
.SS Head2
Contents here
EOMAN
dotest "Formatting", <<"EOMAN";
.PP
\\fBbold\\fP
.PP
\\fIitalic\\fP
.PP
\\f(CWcode\\->with\\->arrows\\fP
EOMAN
dotest "Verbatim", <<"EOMAN";
.SH EXAMPLE
.EX
use v5.14;
t/30output-plain.t view on Meta::CPAN
EOPOD
<<"EOF";
Head1
Head2
Contents here
EOF
dotest "Formatting", pod => <<"EOPOD",
=pod
B<bold> B<< <bold> >>
I<italic>
C<code> C<< code->with->arrows >>
L<link|target://> L<Module::Here>
EOPOD
<<"EOF";
bold <bold>
italic
code code->with->arrows
link Module::Here
EOF
dotest "Verbatim", pod => <<"EOPOD",
=head1 EXAMPLE