modules/pod/markdown.zzm

pod-parser-0.0.1 source code

Package

Name
pod-parser
Version
0.0.1
Uploaded
2026-05-28 11:45:33
Dependencies
Metadata
zuzu-distribution.json
Archive
Download .tar.gz
=encoding utf8

=head1 NAME

pod/markdown - Render parsed POD documents as Markdown.

=head1 SYNOPSIS

  from pod/parser import parse_pod;
  from pod/markdown import PodMarkdown;

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

=head1 DESCRIPTION

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

=head1 EXPORTED CLASSES

=over

=item C<PodMarkdown>

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

=back

=head1 COPYRIGHT AND LICENCE

B<< pod/markdown >> 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 _repeat ( String text, Number count ) {
	let out := "";
	let i := 0;
	while ( i < count ) {
		out _= text;
		i++;
	}
	return out;
}

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

function _indent_block ( String text, String prefix ) {
	let out := [];
	for ( let line in split( text, "\n" ) ) {
		out.push(prefix _ line);
	}
	return join( "\n", 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 _markdown_link ( String inner ) {
	let parts := _split_link(inner);
	let label := _link_label(parts[0]);
	let target := parts[1];
	label := target if label eq "";
	return "[" _ 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 "`" _ trim(text) _ "`";
	}
	if ( marker eq "B" ) {
		return "**" _ text _ "**";
	}
	if ( marker eq "I" ) {
		return "*" _ text _ "*";
	}
	if ( marker eq "L" ) {
		return _markdown_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;
}

class PodMarkdown {
	let Boolean include_for_markdown := true;

	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" ) {
			let level := node.level();
			level := 1 if level < 1;
			level := 6 if level > 6;
			return _repeat( "#", level ) _ " " _ _inline( node.text() );
		}

		if ( kind eq "paragraph" ) {
			return _inline( node.text() );
		}

		if ( kind eq "verbatim" ) {
			return _indent_block( node.text(), _repeat( "    ", depth + 1 ) );
		}

		if ( kind eq "list" ) {
			return join( "\n", self._render_list_items( node, depth ) );
		}

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

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

		if ( kind eq "block" ) {
			return "" if node.target() ne "markdown";
			return join( "\n\n", self._render_children( node, depth ) );
		}

		return _inline( node.text() );
	}

	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 prefix := _repeat( "  ", depth ) _ "- ";
		let parts := [];
		let body := _inline( item.text() );
		body := " " if body eq "";
		parts.push(prefix _ body);

		for ( let child in item.children() ) {
			let rendered := self._render_node( child, depth + 1 );
			if ( trim(rendered) ne "" ) {
				parts.push(
					child.type() eq "verbatim"
						? rendered
						: _indent_block( rendered, _repeat( "  ", depth + 1 ) ),
				);
			}
		}

		return join( "\n\n", parts );
	}
}