modules/pod/html.zzm

pod-parser-0.0.2 source code

Package

Name
pod-parser
Version
0.0.2
Uploaded
2026-05-28 14:18:09
Dependencies
Metadata
zuzu-distribution.json
Archive
Download .tar.gz
=encoding utf8

=head1 NAME

pod/html - Render parsed POD documents as HTML.

=head1 SYNOPSIS

  from pod/parser import parse_pod;
  from pod/html import PodHTML;

  let doc := parse_pod("=head1 NAME\n\nExample\n\n=cut\n");
  say( ( new PodHTML() ).render(doc) );

=head1 DESCRIPTION

This pure-Zuzu module renders C<pod/parser> C<PodDocument> objects to
HTML. It renders headings, paragraphs, verbatim blocks, lists, items, and
HTML-targeted C<=for> and C<=begin> blocks.

=head1 EXPORTED CLASSES

=over

=item C<PodHTML>

Renderer class with C<render> and C<render_node> methods.

=back

=head1 COPYRIGHT AND LICENCE

B<< pod/html >> 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;

function _nonempty_push ( Array out, text ) {
	if ( text != null and trim(text) ne "" ) {
		out.push(text);
	}
}

function _repeat ( String text, Number count ) {
	let out := "";
	let i := 0;
	while ( i < count ) {
		out _= text;
		i++;
	}
	return out;
}

function _escape_html ( text ) {
	let source := "" _ text;
	let out := "";
	let i := 0;
	while ( i < length source ) {
		let ch := substr( source, i, 1 );
		if ( ch eq "&" ) {
			out _= "&amp;";
		}
		else if ( ch eq "<" ) {
			out _= "&lt;";
		}
		else if ( ch eq ">" ) {
			out _= "&gt;";
		}
		else if ( ch eq "\"" ) {
			out _= "&quot;";
		}
		else if ( ch eq "'" ) {
			out _= "&#39;";
		}
		else {
			out _= ch;
		}
		i++;
	}
	return out;
}

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 _module_search_url ( String module ) {
	return "https://zuzulang.org/modules?q="
		_ replace( module, "/", "%2F", "g" )
		_ "&direct=1";
}

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 _html_link ( String inner ) {
	let parts := _split_link(inner);
	let label := _link_label(parts[0]);
	let target := parts[1];
	label := target if label eq "";
	return "<a href=\"" _ _escape_html( _link_target(target) ) _ "\">"
		_ _escape_html(label) _ "</a>";
}

function _format_inner ( String marker, String inner, Boolean trim_inner ) {
	let text := trim_inner ? trim(inner) : inner;
	if ( marker eq "C" ) {
		return "<code>" _ _escape_html( trim(text) ) _ "</code>";
	}
	if ( marker eq "B" ) {
		return "<strong>" _ _escape_html(text) _ "</strong>";
	}
	if ( marker eq "I" ) {
		return "<em>" _ _escape_html(text) _ "</em>";
	}
	if ( marker eq "L" ) {
		return _html_link(text);
	}
	return _escape_html(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 _= _escape_html( substr( source, i, 1 ) );
		i++;
	}

	return out;
}

class PodHTML {
	let Boolean include_for_html := true;

	method render ( PodDocument document ) {
		return join( "\n", self._render_children(document) );
	}

	method render_node ( PodNode node ) {
		return self._render_node(node);
	}

	method _render_children ( PodNode parent ) {
		let out := [];
		for ( let child in parent.children() ) {
			_nonempty_push( out, self._render_node(child) );
		}
		return out;
	}

	method _render_node ( PodNode node ) {
		let kind := node.type();

		if ( kind eq "document" ) {
			return join( "\n", self._render_children(node) );
		}

		if ( kind eq "encoding" or kind eq "pod" or kind eq "command" ) {
			return "";
		}

		if ( kind eq "heading" ) {
			let level := node.level();
			level := 1 if level < 1;
			level := 6 if level > 6;
			return "<h" _ level _ ">" _ _inline( node.text() )
				_ "</h" _ level _ ">";
		}

		if ( kind eq "paragraph" ) {
			return "<p>" _ _inline( node.text() ) _ "</p>";
		}

		if ( kind eq "verbatim" ) {
			return "<pre><code>" _ _escape_html( node.text() )
				_ "</code></pre>";
		}

		if ( kind eq "list" ) {
			return "<ul>\n" _ join( "\n", self._render_list_items(node) )
				_ "\n</ul>";
		}

		if ( kind eq "item" ) {
			return self._render_item(node);
		}

		if ( kind eq "for" ) {
			return "" if not include_for_html;
			return node.target() eq "html" ? node.text() : "";
		}

		if ( kind eq "block" ) {
			return "" if node.target() ne "html";
			return node.text_content();
		}

		return _inline( node.text() );
	}

	method _render_list_items ( PodNode list ) {
		let out := [];
		for ( let child in list.children() ) {
			if ( child.type() eq "item" ) {
				_nonempty_push( out, self._render_item(child) );
			}
			else {
				_nonempty_push( out, self._render_node(child) );
			}
		}
		return out;
	}

	method _render_item ( PodNode item ) {
		let parts := [];
		let body := _inline( item.text() );
		body := " " if body eq "";
		parts.push(body);

		for ( let child in item.children() ) {
			_nonempty_push( parts, self._render_node(child) );
		}

		return "<li>" _ join( "\n", parts ) _ "</li>";
	}
}