=encoding utf8
=head1 NAME
pod/ansi - Render parsed POD documents as ANSI terminal text.
=head1 SYNOPSIS
from pod/parser import parse_pod;
from pod/ansi import PodANSI;
let doc := parse_pod("=head1 NAME\n\nExample\n\n=cut\n");
say( ( new PodANSI() ).render(doc) );
=head1 DESCRIPTION
This pure-Zuzu module renders C<pod/parser> C<PodDocument> objects to
ANSI-styled terminal text. Headings are bright bold white, C<B<...>> is
bold, C<I<...>> is underlined, C<C<...>> and verbatim blocks are cyan,
and C<L<...>> links are yellow.
Text paragraphs and list items are wrapped to C<width>, which defaults
to 80 columns.
=head1 EXPORTED CLASSES
=over
=item C<PodANSI>
Renderer class with C<render> and C<render_node> methods.
=back
=head1 COPYRIGHT AND LICENCE
B<< pod/ansi >> is copyright Toby Inkster.
It is free software; you may redistribute it and/or modify it under
the terms of either the Artistic License 1.0 or the GNU General Public
License version 2.
=cut
from pod/parser import PodDocument, PodNode;
from std/string import index, join, replace, split, substr, trim;
from std/tui import ansi_esc;
let String _ESC := ansi_esc();
let String _RESET := _ESC _ "[0m";
let String _BOLD := _ESC _ "[1m";
let String _UNDERLINE := _ESC _ "[4m";
let String _CYAN := _ESC _ "[36m";
let String _YELLOW := _ESC _ "[33m";
let String _HEADING := _ESC _ "[1;97m";
function _nonempty_push ( Array out, text ) {
if ( text != null and trim(text) ne "" ) {
out.push(text);
}
}
function _styled ( String code, text ) {
return code _ text _ _RESET;
}
function _repeat ( String text, Number count ) {
let out := "";
let i := 0;
while ( i < count ) {
out _= text;
i++;
}
return out;
}
function _module_search_url ( String module ) {
return "https://zuzulang.org/modules?q="
_ replace( module, "/", "%2F", "g" )
_ "&direct=1";
}
function _split_link ( String inner ) {
let pipe := index( inner, "|" );
if ( pipe < 0 ) {
return [ trim(inner), trim(inner) ];
}
return [
trim( substr( inner, 0, pipe ) ),
trim( substr( inner, pipe + 1 ) ),
];
}
function _slug ( String text ) {
let out := "";
let dash := false;
let source := lc(text);
let i := 0;
while ( i < length source ) {
let ch := substr( source, i, 1 );
if ( ch ~ /^[a-z0-9]$/ ) {
out _= ch;
dash := false;
}
else if ( out ne "" and not dash ) {
out _= "-";
dash := true;
}
i++;
}
return substr( out, 0, length out - 1 ) if dash;
return out;
}
function _link_target ( String raw ) {
let target := trim(raw);
return target if target ~ /^[A-Za-z][A-Za-z0-9+.-]*:/;
if ( length target > 0 and substr( target, 0, 1 ) eq "#" ) {
return target;
}
if ( length target > 0 and substr( target, 0, 1 ) eq "/" ) {
return "#" _ _slug( substr( target, 1 ) );
}
if ( index( target, "/" ) >= 0 ) {
return _module_search_url(target);
}
return "#" _ _slug(target);
}
function _link_label ( String label ) {
let text := trim(label);
if ( length text > 0 and substr( text, 0, 1 ) eq "/" ) {
return substr( text, 1 );
}
return text;
}
function _ansi_link ( String inner ) {
let parts := _split_link(inner);
let label := _link_label(parts[0]);
let target := parts[1];
label := target if label eq "";
return _styled( _YELLOW, label ) _ " <" _ _link_target(target) _ ">";
}
function _format_inner ( String marker, String inner, Boolean trim_inner ) {
let text := trim_inner ? trim(inner) : inner;
if ( marker eq "C" ) {
return _styled( _CYAN, trim(text) );
}
if ( marker eq "B" ) {
return _styled( _BOLD, text );
}
if ( marker eq "I" ) {
return _styled( _UNDERLINE, text );
}
if ( marker eq "L" ) {
return _ansi_link(text);
}
return text;
}
function _inline ( text ) {
let source := "" _ text;
let out := "";
let i := 0;
while ( i < length source ) {
let marker := substr( source, i, 1 );
if (
( marker eq "C" or marker eq "B" or marker eq "I" or marker eq "L" )
and i + 1 < length source
and substr( source, i + 1, 1 ) eq "<"
) {
let delimiter := 1;
while (
i + 1 + delimiter < length source
and substr( source, i + 1 + delimiter, 1 ) eq "<"
) {
delimiter++;
}
let start := i + 1 + delimiter;
let close := index( source, _repeat( ">", delimiter ), start );
if ( close >= 0 ) {
out _= _format_inner(
marker,
substr( source, start, close - start ),
delimiter > 1,
);
i := close + delimiter;
next;
}
}
out _= substr( source, i, 1 );
i++;
}
return out;
}
function _visible_length ( String text ) {
let count := 0;
let i := 0;
while ( i < length text ) {
if (
substr( text, i, 1 ) eq _ESC
and i + 1 < length text
and substr( text, i + 1, 1 ) eq "["
) {
i += 2;
while ( i < length text and not( substr( text, i, 1 ) ~ /^[A-Za-z]$/ ) ) {
i++;
}
i++ if i < length text;
next;
}
count++;
i++;
}
return count;
}
function _wrap_line ( String line, Number width, String prefix ) {
return prefix _ line if width <= 0;
let available := width - _visible_length(prefix);
available := 1 if available < 1;
let words := split( trim(line), " " );
let out := [];
let current := "";
for ( let word in words ) {
next if word eq "";
if ( current eq "" ) {
current := word;
next;
}
if ( _visible_length(current) + 1 + _visible_length(word) > available ) {
out.push(prefix _ current);
current := word;
}
else {
current _= " " _ word;
}
}
out.push(prefix _ current) if current ne "";
return join( "\n", out );
}
function _wrap_text ( String text, Number width, String prefix := "" ) {
let out := [];
for ( let line in split( text, "\n" ) ) {
_nonempty_push( out, _wrap_line( line, width, prefix ) );
}
return join( "\n", out );
}
function _wrap_hanging (
String text,
Number width,
String first_prefix,
String next_prefix,
) {
let lines := split( _wrap_line( text, width, first_prefix ), "\n" );
let out := [];
let i := 0;
while ( i < lines.length() ) {
if ( i == 0 ) {
out.push(lines[i]);
}
else {
let line := lines[i];
if ( substr( line, 0, length first_prefix ) eq first_prefix ) {
line := substr( line, length first_prefix );
}
out.push(next_prefix _ line);
}
i++;
}
return join( "\n", out );
}
function _item_parts ( String body, Number depth ) {
let indent := _repeat( " ", depth );
if ( length body >= 2 and substr( body, 0, 2 ) eq "* " ) {
return [ indent _ "* ", substr( body, 2 ) ];
}
return [ indent _ "- ", body ];
}
class PodANSI {
let Number width := 80;
method render ( PodDocument document ) {
return join( "\n\n", self._render_children( document, 0 ) );
}
method render_node ( PodNode node ) {
return self._render_node( node, 0 );
}
method _render_children ( PodNode parent, Number depth ) {
let out := [];
for ( let child in parent.children() ) {
_nonempty_push( out, self._render_node( child, depth ) );
}
return out;
}
method _render_node ( PodNode node, Number depth ) {
let kind := node.type();
if ( kind eq "document" ) {
return join( "\n\n", self._render_children( node, depth ) );
}
if ( kind eq "encoding" or kind eq "pod" or kind eq "command" ) {
return "";
}
if ( kind eq "heading" ) {
return _wrap_text( _styled( _HEADING, node.text() ), width );
}
if ( kind eq "paragraph" ) {
return _wrap_text( _inline( node.text() ), width );
}
if ( kind eq "verbatim" ) {
let out := [];
for ( let line in split( node.text(), "\n" ) ) {
out.push( _styled( _CYAN, line ) );
}
return join( "\n", out );
}
if ( kind eq "list" ) {
return join( "\n\n", self._render_list_items( node, depth ) );
}
if ( kind eq "item" ) {
return self._render_item( node, depth );
}
if ( kind eq "for" ) {
return node.target() eq "text" or node.target() eq "ansi"
? node.text()
: "";
}
if ( kind eq "block" ) {
return node.target() eq "text" or node.target() eq "ansi"
? node.text_content()
: "";
}
return _wrap_text( _inline( node.text() ), width );
}
method _render_list_items ( PodNode list, Number depth ) {
let out := [];
for ( let child in list.children() ) {
if ( child.type() eq "item" ) {
_nonempty_push( out, self._render_item( child, depth ) );
}
else {
_nonempty_push( out, self._render_node( child, depth + 1 ) );
}
}
return out;
}
method _render_item ( PodNode item, Number depth ) {
let out := [];
let body := _inline( item.text() );
let item_parts := _item_parts( body, depth );
let prefix := item_parts[0];
body := item_parts[1];
let continuation := _repeat( " ", depth ) _ " ";
body := " " if body eq "";
out.push( _wrap_hanging( body, width, prefix, continuation ) );
let previous_was_list := false;
for ( let child in item.children() ) {
if ( child.type() ne "list" and previous_was_list ) {
out.push("");
previous_was_list := false;
}
if ( child.type() eq "paragraph" ) {
out.push( _wrap_text( _inline( child.text() ), width, continuation ) );
next;
}
if ( child.type() eq "verbatim" ) {
for ( let line in split( child.text(), "\n" ) ) {
out.push( continuation _ _styled( _CYAN, line ) );
}
next;
}
if ( child.type() eq "list" ) {
_nonempty_push( out, self._render_node( child, depth + 1 ) );
previous_was_list := true;
next;
}
let rendered := self._render_node( child, depth + 1 );
if ( trim(rendered) ne "" ) {
out.push( _wrap_text( rendered, width, continuation ) );
}
}
return join( "\n", out );
}
}
modules/pod/ansi.zzm
pod-parser-0.0.1 source code
Package
- Name
- pod-parser
- Version
- 0.0.1
- Uploaded
- 2026-05-28 11:45:33
- Dependencies
-
-
std/getopt>= 0 -
std/io>= 0 -
std/string>= 0 -
std/tui>= 0
-
- Metadata
- zuzu-distribution.json
- Archive
- Download .tar.gz