### ----------------------------------------------------
### -- Rakuast::RakuDoc::Render
### -- Licenses: Artistic-2.0
### -- Authors: Richard Hainsworth
### -- File: lib/RakuDoc/Render.rakumod
### ----------------------------------------------------
### -- Chunk 3 of 4
 ｢{ $!scoped-data.last-starter }｣ with heading ｢{ $!scoped-data.last-title }｣.")
                if %config<error>
        }
        else { # not known so create another warning
            $prs.warnings.push(
                '｢' ~ $ast.type ~ '｣' ~ ' is not a valid builtin block, is it a misspelt Custom block?'
                ~ " in block ｢{ $!scoped-data.last-starter }｣ with heading ｢{ $!scoped-data.last-title }｣."
                ) if %config<error>
        }
        my $target = $.name-id($ast.type);
        $prs.body ~= %!templates<unknown>( %( :$contents, :block-name($ast.type), :$target ) )
    }
    #| Semantic blocks defined by spelling
    #| embedded content is rendered and passed to template as contents
    #| rendered contents is added to the semantic structure
    #| If :hidden is True, then the string is not added to .body
    #| Unless :hidden, Block name is added to ToC at level 1, unless overriden by toc/caption/headlevel
    #| TITLE & SUBTITLE by default :hidden is True and added to $*prs separately
    #| All other SEMANTIC blocks are :!hidden by default
    method gen-semantics($ast, $parify) {
        my $block-name = $ast.type;
        my %config = $.merged-config($ast, $block-name);
        $!scoped-data.last-title( $block-name );
        # treat all semantic blocks as a heading level 1 unless otherwise specified
        my $caption = %config<caption> // $block-name;
        my $level = %config<headlevel> // 1;
        my $hidden;
        my $contents = $.contents($ast, $parify).trim;
        $contents ~= $.complete-item-list ~ $.complete-defn-list
            ~ $.complete-numitem-list ~ $.complete-numdefn-list;
        my $prs := $*prs;
        my $rv;
        given $ast.type {
            when 'TITLE' {
                $hidden = True; # hide by default
                $hidden = $_ with %config<hidden>;
                $!current.title = $contents.Str;
                my $target = $!current.title-target = $.name-id( $contents.Str);
                # allows for TITLE to have its own template
                if %!templates<TITLE>:exists {
                    $rv = %!templates<TITLE>( %( :$level, :$contents, :$caption, :$target, %config ) )
                }
                else {
                    $rv = %!templates<semantic>( %( :$level, :$contents, :$caption, :$target, :$hidden, %config ) )
                }
            }
            when 'SUBTITLE' {
                $hidden = True; # hide by default
                $hidden = $_ with %config<hidden>;
                $!current.subtitle = $contents.Str;
                my $target = $.name-id($contents.Str);
                if %!templates<SUBTITLE>:exists {
                    $rv = %!templates<SUBTITLE>( %( :$level, :$contents, :$caption, :$target, :$hidden, %config ) )
                }
                else {
                    $rv = %!templates<semantic>( %( :$level, :$contents, :$caption, :$target, :$hidden, %config ) )
                }
            }
            default {
                $hidden = %config<hidden>.so // False;
                # other SEMANTIC by default rendered in place
                # allows for a plugin to add a SEMANTIC blockname to templates
                my $template = %!templates{ $block-name }:exists ?? $block-name !! 'semantic';
                my $target = $.name-id($block-name);
                my $id = '';
                with %config<id> {
                    if self.is-target-unique( $_ ) {
                        self.register-target( $_ );
                        $id = $_
                    }
                    else {
                        $prs.warnings.push(
                            "Attempt to register already existing id ｢$_｣ as new target in ｢$block-name｣"
                            ~ " in block ｢{ $!scoped-data.last-starter }｣ with heading ｢{ $!scoped-data.last-title }｣."
                            ) if %config<error>
                    }
                }
                $rv = %!templates{$template}(
                    %( :$level, :$caption, :$hidden, :$target, :$contents, :$id, %config )
                );
                $prs.toc.push(  %( :$caption, :$target, :$level ) ) unless $hidden;
            }
        }
        $prs.semantics{ $block-name } = [] unless $prs.semantics{ $block-name }:exists;
        $prs.semantics{ $block-name }.push: $rv;
        $prs.body ~= $rv unless $hidden;
    }
    method gen-custom($ast, $parify) {
        # Custom blocks are defined by their spelling
        # - the block name is added to ToC at level 1 unless changed by toc/caption/headlevel
        # If a template exists with the block name
        # - provides content verbatim to template as raw
        # - provides content rendered to template as contents
        # If NOT,
        # - the block content is rendered as verbatim text
        # - the content is rendered with 'unknown' template
        # - a warning is issued
        my $block-name = $ast.type;
        my %config = $.merged-config( $ast, $block-name);
        my $prs := $*prs;
        my $caption = %config<caption>:delete // $block-name;
        my $level = %config<headlevel>:delete // 1;
        $level = 1 unless $level >= 1;
        my $numeration = '';
        my $id = '';
        with %config<id> {
            if self.is-target-unique( $_ ) {
                self.register-target( $_ );
                $id = $_
            }
            else {
                $prs.warnings.push(
                    "Attempt to register already existing id ｢$_｣ as new target in ｢$block-name｣"
                    ~ " in block ｢{ $!scoped-data.last-starter }｣ with heading ｢{ $!scoped-data.last-title }｣."
                ) if %config<error>
            }
        }
        my $target = %config<id>:delete // $.name-id($caption);
        my $toc = %config<toc>:delete // True;
        # attach numeration to caption and contents separately, allowing template
        # developer to add numeration to caption if wanted by changing the template
        $prs.toc.push(
            { :$caption, :$target, :$level, :$numeration }
        ) if $toc;
        if %!templates{ $block-name }:exists {
            my $contents = $.contents( $ast, $parify );
            my $raw = $ast.paragraphs.Str.join;
            $prs.body ~= %!templates{ $block-name }( %( :$contents, :$raw, :$level, :$target, :$caption, :$id, :$toc, %config ) )
        }
        else {
            # by spec, the name of an unrecognised Custom is treated like =head1
            # the contents are treated like =code
            my $contents = $ast.DEPARSE;
            $contents = %config<alt> if %config<alt>:exists;
            $prs.warnings.push(
            "No template exists for custom block ｢$block-name｣. It has been rendered as unknown"
                ~ " in block ｢{ $!scoped-data.last-starter }｣ with heading ｢{ $!scoped-data.last-title }｣."
            ) if %config<error>;
            $prs.body ~= %!templates<unknown>( %( :$contents, :$block-name, :$target, :$caption) )
        }
    }
    # directive type methods
    method manage-config($ast) {
        my %options = $ast.resolved-config;
        my $name = $ast.paragraphs[0].Str;
        $name = $name ~ '1' if $name eq <item head numitem numhead>.any;
        $!scoped-data.config( { $name => %options } );
    }
    method manage-alias($ast) {
        my %config = $ast.resolved-config;
        my $prs := $*prs;
        if $ast.paragraphs.elems >= 2 {
            my $term = $ast.paragraphs[0].Str; # it should be a string without embedded codes
            my ProcessedState $*prs .= new;
            $ast.paragraphs[1 .. *-1 ].map({ $.handle( $_ ) });
            my $prs := $*prs;
            my $expansion = $prs.body.trim-trailing;
            $expansion ~= $.complete-item-list;
            $expansion ~= $.complete-defn-list;
            $expansion ~= $.complete-numitem-list;
            $expansion ~= $.complete-numdefn-list;
            $!scoped-data.aliases{ $term } = $expansion;
            $prs.body .= new;
            CALLERS::<$*prs> += $prs;
        }
        else {
            $prs.warnings.push("Invalid alias ｢{ $ast.Str }｣") if %config<error>
        }
    }
    ## completion methods

    #| finalise the rendering of footnotes
    #| the numbering only happens when all footnotes are collected
    #| completes the PCell in the body
    method complete-footnotes {
        for $!current.footnotes.kv -> $n, %data {
            %data<fnNumber> = $n + 1;
            $!register.add-payload( :payload($n + 1), :id( 'fn_num_' ~ %data<retTarget> ) );
        }
    }
    #| takes the index structure and returns an ordered list of items to be rendered
    sub serialise-index( %h, $n, $max ) {
        return unless (%h.elems and $n <= $max);
        my @rv;
        for %h.sort( *.key )>>.kv -> ( $label, %indexed) {
            my @inner = $n, $label, %indexed<refs>;
            @rv.push: @inner;
            if serialise-index( %indexed<sub-index>, $n + 1, $max ) -> $_ { @rv.append($_.list) }
        }
        @rv
    }
    sub stringify-index( %h ) {
        for %h.pairs {
            for .value<refs>.list {
                .<place> .= Str
            }
            stringify-index( .value<sub-index>.hash ) if .value<sub-index>.elems.so
        }
    }
    method complete-index( :$spec, :$caption --> PStr ) {
        my $max; # the maximum number of index levels. Shouldn't be more than around 5
        if $spec eq '*' { $max = 100 }
        else { $max = $spec.EVAL.list.max }
        my @index-list;
        if serialise-index( $!current.index , 1, $max ) -> $_ {
            @index-list = .map({
                %!templates<index-item>( %( :level( .[0] ), :entry( .[1] ), :refs( .[2] ) ) )
            })
        }
        PStr.new( @index-list.elems ?? %!templates<index>( %(:@index-list, :$caption ))
                                    !! '')
    }
    #| renders the complete toc
    method complete-toc( :$spec, :$caption --> PStr ) {
        my Set $levels;
        if $spec eq '*' { $levels .=new: (^10).list }
        else { $levels .= new: $spec.EVAL.list }
        my @toc-list = gather for $!current.toc -> $toc-entry {
            take %!templates<toc-item>( %( :$toc-entry , ) ) if $levels{ +$toc-entry<level> }
        }
        PStr.new( @toc-list.elems ?? %!templates<toc>( %(:@toc-list, :toc( $!current.toc), :$caption ) )
                                  !! '' )
    }
    #| finalises all the heading numerations
    method complete-heading-numerations() {
        return unless $*prs.head-numbering.elems;
        my Numeration $heads .= new;
        for $*prs.head-numbering -> ( $id, $level  ) {
            my $payload;
            $payload = $heads.inc( $level ).Str if $id.starts-with('heading');
            $!register.add-payload( :$payload, :$id )
        }
    }
    #| finalises rendering of the item list in $*prs
    method complete-item-list() {
        return '' unless $*prs.items.elems; # do nothing if no accumulated items
        my $rv = %!templates<item-list>(
            %( :item-list($*prs.items), )
        );
        $*prs.items = ();
        $rv
    }
    #| finalises rendering of a defn list in $*prs
    method complete-defn-list() {
        return '' unless $*prs.defns.elems;
        my $rv = %!templates<defn-list>(
            %( :defn-list($*prs.defns), )
        );
        $*prs.defns = ();
        $rv
    }
    #| finalises rendering of the item list in $*prs
    method complete-numitem-list() {
        return '' unless $*prs.numitems.elems;
        my $rv = %!templates<numitem-list>(
            %( :numitem-list($*prs.numitems), )
        );
        $*prs.numitems = ();
        $rv
    }
    #| finalises rendering of a defn list in $*prs
    method complete-numdefn-list() {
        return '' unless $*prs.numdefns.elems;
        my $rv = %!templates<numdefn-list>(
            %( :numdefn-list($*prs.numdefns), )
        );
        $*prs.numdefns = ();
        $rv
    }
    # helper methods
    method is-target-unique($targ --> Bool) {
        $!current.targets{$targ}.not
    }
    method register-target($targ) {
        $!current.targets{$targ}++;
        $targ
    }
    #| The 'contents' method is called when $ast.paragraphs is a sequence.
    #| The $*prs for a set of paragraphs is new to collect all the
    #| associated data. The body of the contents must then be
    #| incorporated using the template of the block calling content
    #| when parify, strings are considered paragraphs
    method contents($ast, Bool $parify ) {
        my ProcessedState $*prs .= new;
        if $parify {
            $.handle( $_ ~~ Str ?? RakuAST::Doc::Paragraph.new($_ ) !! $_ )
                for $ast.paragraphs
        }
        else {
            $.handle( $_ ) for $ast.paragraphs
        }
        my $prs := $*prs;
        my $text = $prs.body.trim-trailing;
        $prs.body .= new;
        CALLERS::<$*prs> += $prs;
        $text
    }
    #| similar to contents but expects atoms structure
    method markup-contents($ast, :$meta = False) {
        my ProcessedState $*prs .= new;
        if $meta { for $ast.meta { $.handle($_) } }
        else { for $ast.atoms { $.handle($_) } }
        my $prs := $*prs;
        my $text = $prs.body;
        $prs.body .= new;
        CALLERS::<$*prs> += $prs;
        $text
    }
    #| get config merged from the ast and scoped data
    #| handle generic metadata options such as delta
    method merged-config( $ast, $block-name --> Hash ) {
        my %config;
        if $ast.defined && $ast.config {
            %config = .resolved-config with $ast;
            for ($ast.config.keys (-) %config.keys).keys -> $k { %config{$k} = $ast.config{$k}.Str }
        }
        my %scoped = $!scoped-data.config;
        %scoped{ $block-name }.pairs.map({
            %config{ .key } = .value unless %config{ .key }:exists
        });
        %scoped{ '*' }.pairs.map({
            %config{ .key } = .value unless %config{ .key }:exists
        });
        %config<error> = True unless %config<error>:exists;
        if %config<delta>:exists {
            my $contents = %config<delta>:delete;
            if $contents.join(' ') ~~ / (<-[;]>+) ';'? ( .* ) $ / {
                %config<delta> = %!templates<delta>(%( :note( ~$1.trim), :versions(~$0.trim) ));
            }
            else {
                $*prs.warnings.push("The delta option is ignored because it must have the form / 'v' \\S+ \\s* (['|'] .+)? \$ / ｢{ ~$ast.DEPARSE }｣")
                if %config<error>
            }
        }
        else { %config<delta> = '' }
        %config
    }

    ## A set of methods to generate anchors / targets
    ## Output formats, eg. MarkDown and HTML, have different
    ## criteria. HTML rendering of Raku documentation has its
    ## own legacy algorithms
    ## So, different methods are used for
    ## - names (blocks) to be included in ToC, which needs to target the block
    ## - footnotes
    ## - indexed text, where Raku HTML has anchors that depend on context and meta
    ## - external links to other documents, which do not have to be unique

    #| Escape characters in a string, needs to be over-ridden
    multi method escape( Str:D $s ) { $s }
    #| Stringify if not string
    multi method escape( $s ) { self.escape( $s.Str ) }
    #| mangle an id to make sure it will be a valid id in the output
    method mangle( $s ) { self.escape( $s ).subst(/ \s /, '_', :g) }

    #| name-id takes an ast
    #| returns a unique Str to be used as an anchor / target
    #| Used by any name (block) that is placed in the ToC
    #| Also used for the main anchor in the text for a footnote
    #| Not called if an :id is specified in the source
    #| This method should be sub-classed by Renderers for different outputs
    #| renderers can use method is-target-unique to test for uniqueness
    method name-id($ast --> Str) {
        my $target = self.mangle($ast.Str.trim);
        return self.register-target($target) if $.is-target-unique($target);
        my @rejects = $target, ;
        # if plain target is rejected, then start adding a suffix
        $target ~= '_0';
        ++$target while $target ~~ any(@rejects);
        self.register-target($target);
    }

    #| Like name-id, index-id returns a unique Str to be used as a target
    #| Target should be unique
    #| Should be sub-classed by Renderers
    method index-id(:$contents) {
        my $target = 'index-entry-' ~ self.mangle($contents.Str.trim);
        return self.register-target($target) if $.is-target-unique($target);
        my @rejects = $target, ;
        # if plain target is rejected, then start adding a suffix
        $target ~= '_0';
        $target += 1 while $target ~~ any(@rejects);
        self.register-target($target);
    }

    #| Like name-id, local-heading returns a Str to be used as a target
    #| A local-heading is assumed to exist because specified by document author
    #| Should be sub-classed by Renderers
    method local-heading($ast) {
        self.mangle($ast.Str.trim);
    }

    method para-target( $contents ) {
        my $n = $!current.source-data<paragraph-id-length>;
        sha1-hex($contents.Str).substr(* - $n)
    }

    # text helpers adapted from Liz's RakuDoc::To::Text
    # colorless ANSI constants
    my constant RESET = "\e[0m";
    my constant BOLD-ON = "\e[1m";
    my constant BOLD-OFF = "\e[22m";
    my constant ITALIC-ON = "\e[3m";
    my constant ITALIC-OFF = "\e[23m";
    my constant UNDERLINE-ON = "\e[4m";
    my constant UNDERLINE-OFF = "\e[24m";
    my constant HEADING-ON = "\e[21m";
    my constant HEADING-OFF = "\e[24m";
    my constant TITLE-ON = "\e[4:3m";
    my constant TITLE-OFF = "\e[4:0m";
    my constant REPLACE-ON = "\e[5m";
    my constant REPLACE-OFF = "\e[25m";
    my constant INDEXED-ON = "\e[7m";
    my constant INDEXED-OFF = "\e[27m";
    my constant CODE-ON = "\e[7m";
    my constant CODE-OFF = "\e[27m";
    my constant STRIKE-ON = "\e[9m";
    my constant STRIKE-OFF = "\e[29m";
    my constant SUPERSCR-ON = "\e[48;5;78m\e[73m";
    my constant SUBSCR-ON = "\e[48;5;80m\e[74m";
    my constant SUBSCR-OFF = "\e[75m\e[39;49m";
    my constant SUPERSCR-OFF = "\e[75m\e[39;49m";
    my constant INDEX-ENTRY-ON = "\e[48;5;2m";
    my constant INDEX-ENTRY-OFF = "\e[39;49m";
    my constant KEYBOARD-ON = "\e[48;5;5m";
    my constant KEYBOARD-OFF = "\e[39;49m";
    my constant TERMINAL-ON = "\e[48;5;6m";
    my constant TERMINAL-OFF = "\e[39;49m";
    my constant FOOTNOTE-ON = "\e[48;5;214m\e[38;5;0m";
    my constant FOOTNOTE-OFF = "\e[39;49m";
    my constant LINK-TEXT-ON = "\e[48;5;227m\e[38;5;0m";
    my constant LINK-TEXT-OFF = "\e[39;49m";
    my constant LINK-ON = "\e[38;5;227m\e[48;5;0m";
    my constant LINK-OFF = "\e[39;49m";
    my constant DEVEL-TEXT-ON = "\e[48;5;216m\e[38;5;0m";
    my constant DEVEL-TEXT-OFF = "\e[39;49m";
    my constant DEVEL-VERSION-ON = "\e[38;5;196m\e[48;5;17m";
    my constant DEVEL-VERSION-OFF = "\e[39;49m";
    my constant DEVEL-NOTE-ON = "\e[38;5;161m\e[48;5;17m";
    my constant DEVEL-NOTE-OFF = "\e[39;49m";
    my constant DEFN-TERM-ON = "\e[1m";
    my constant DEFN-TERM-OFF = "\e[22m";
    my constant DEFN-TEXT-ON = "  \e[48;5;243m";
    my constant DEFN-TEXT-OFF = "\e[39;49m";
    my constant BAD-MARK-ON = "\e[38;5;117m\e[48;5;0m";
    my constant BAD-MARK-OFF = "\e[39;49m";
    my constant @bullets = <<\x2022 \x25b9 \x2023 \x2043 \x2219>> ;

    #| returns a set of text templates
    multi method default-text-templates {
        %(
            #| special key to name template set
            _name => -> %, $ { 'default text templates' },
            #| renders =code blocks
            code => -> %prm, $tmpl {
                my $del = %prm<delta> // '';
                PStr.new: $del ~ "\n  --- code --- \n"
                ~ %prm<contents>
                ~ "\n  --- ----- ---\n"
            },
            #| renders =input block
            input => -> %prm, $tmpl {
                my $del = %prm<delta> // '';
                PStr.new: $del ~ "\n  --- input --- \n"
                ~ %prm<contents>
                ~ "\n  --- ------ ---\n"
            },
            #| renders =output block
            output => -> %prm, $tmpl {
                my $del = %prm<delta> // '';
                PStr.new: $del ~ "\n  --- output --- \n"
                ~ %prm<contents>
                ~ "\n  --- ------ ---\n"
             },
            #| renders =comment block
            comment => -> %prm, $tmpl { '' },
            #| renders =formula block
            formula => -> %prm, $tmpl {
                my $indent = %prm<level> > 5 ?? 4 !! (%prm<level> - 1) * 2;
                my $del = %prm<delta> // '';
                "\n" ~ ' ' x $indent  ~ HEADING-ON ~ %prm<caption> ~  HEADING-OFF ~ "\n\n" ~
                $del ~ %prm<formula> ~ "\n\n"
            },
            #| renders =head block
            head => -> %prm, $tmpl {
                my $indent = %prm<level> > 5 ?? 4 !! (%prm<level> - 1) * 2;
                my $del = %prm<delta> // '';
                "\n" ~ ' ' x $indent  ~ HEADING-ON ~ BOLD-ON ~ %prm<contents> ~ BOLD-OFF ~ HEADING-OFF ~
                "\n" ~ $del
            },
            #| renders =numhead block
            numhead => -> %prm, $tmpl {
                my $del = %prm<delta> // '';
                my $indent = %prm<level> > 5 ?? 4 !! (%prm<level> - 1) * 2;
                my $title = %prm<numeration> ~ ' ' ~ %prm<contents>;
                "\n" ~ ' ' x $indent ~ HEADING-ON ~ BOLD-ON ~ $title ~ BOLD-OFF ~  HEADING-OFF ~
                "\n" ~ $del
            },
            #| renders the numeration part for a toc
            toc-numeration => -> %prm, $tmpl { %prm<contents> },
            #| rendering the content from the :delta option
            #| see inline variant markup-Δ
            delta => -> %prm, $tmpl {
                ( %prm<note> ??
                       DEVEL-NOTE-ON ~ %prm<note> ~ DEVEL-NOTE-OFF
                    !! ''
                ) ~
                DEVEL-VERSION-ON ~
                " for " ~
                %prm<versions> ~ DEVEL-VERSION-OFF ~
                "\n\n"
            },
            #| renders =defn block
            defn => -> %prm, $tmpl {
                DEFN-TERM-ON ~ %prm<term> ~ DEFN-TERM-OFF ~ "\n" ~
                DEFN-TEXT-ON ~ %prm<contents> ~ DEFN-TEXT-OFF ~ "\n"
            },
            #| renders =numdefn block
            #| special template to render a defn list data structure
            defn-list => -> %prm, $tmpl { [~] %prm<defn-list> },
            #| special template to render a numbered defn list data structure
            numdefn => -> %prm, $tmpl {
                DEFN-TERM-ON ~ %prm<numeration> ~ ' ' ~ %prm<term> ~ DEFN-TERM-OFF ~ "\n" ~
                DEFN-TEXT-ON ~ %prm<contents> ~ DEFN-TEXT-OFF ~ "\n"
            },
            #| special template to render a numbered item list data structure
            numdefn-list => -> %prm, $tmpl { [~] %prm<numdefn-list> },
            #| renders =item block
            item => -> %prm, $tmpl {
                my $num = %prm<level> - 1;
                my $indent = ' ' x %prm<level>;
                $num = @bullets.elems - 1 if $num >= @bullets.elems;
                my $bullet = %prm<bullet> // @bullets[ $num ];
                $indent ~ $bullet ~ ' ' ~ %prm<contents> ~ "\n"
            },
            #| special template to render an item list data structure
            item-list => -> %prm, $tmpl {
                [~] %prm<item-list>
            },
            #| renders =numitem block
            numitem => -> %prm, $tmpl {
                %prm<numeration> ~ ' ' ~ %prm<contents> ~ "\n"
            },
            #| special template to render a numbered item list data structure
            numitem-list => -> %prm, $tmpl {
                [~] %prm<numitem-list>
            },
            #| renders =nested block
            nested => -> %prm, $tmpl {
                PStr.new: "\t" ~ %prm<contents> ~ "\n\n"
            },
            #| renders =para block
            para => -> %prm, $tmpl {
                PStr.new: %prm<contents> ~ "\n\n"
            },
            #| renders =place block
            place => -> %prm, $tmpl {
                my $level = %prm<headlevel> // 1;
                my $rv = $tmpl('head', %(:$level, :contents(%prm<caption>), :delta(%prm<delta>)));
                if %prm<content-type>.contains('text') {
                    $rv ~= %prm<contents>
                }
                else {
                    $rv ~= "Link returned {%prm<content-type>}, which cannot be rendered"
                }
                $rv ~= "\n\n";
            },
            #| renders =rakudoc block
            rakudoc => -> %prm, $tmpl { %prm<contents> ~ "\n" }, #pass through without change
            #| renders =section block
            section => -> %prm, $tmpl {
                (%prm<delta> // '') ~
                %prm<contents> ~ "\n"
            },
            #| renders =SEMANTIC block, if not otherwise given
            semantic => -> %prm, $tmpl {
                my $indent = %prm<level> > 5 ?? 4 !! (%prm<level> - 1) * 2;
                my $del = %prm<delta> // '';
                my $head = "\n" ~ ' ' x $indent  ~ HEADING-ON ~ BOLD-ON ~ %prm<caption> ~ BOLD-OFF ~ HEADING-OFF ~ "\n";
                ( $head unless %prm<hidden> ) ~
                $del ~
                %prm<contents> ~ "\n"
            },
            #| renders =pod block
            pod => -> %prm, $tmpl { %prm<contents> },
            #| renders =table block
            table => -> %prm, $tmpl {
                use Text::MiscUtils::Layout;
                my $del = %prm<delta> // '';
                my $caption = HEADING-ON ~ %prm<caption> ~ HEADING-OFF;
                my $cap-width = duospace-width($caption);
                if %prm<procedural> {
                    # calculate column widths naively, will include possible markup, and
                    # will fail if embedded tables
                    # TODO comply with justification, now right-justify col-head, top-justify row labels.
                    my @col-wids;
                    my $wid;
                    for %prm<grid>.list -> @row {
                        for @row.kv -> $n, %cell {
                            next if %cell<no-cell>;
                            $wid = duospace-width(%cell<data>.Str) + 2;
                            @col-wids[$n] = $wid if $wid > (@col-wids[$n] // 0)
                        }
                    }
                    my $table-wid = (+@col-wids * 3) + 4 + [+] @col-wids;
                    my @rendered-grid;
                    my $col-count;
                    for %prm<grid>.kv -> $r, @row {
                        $col-count = 0;
                        for @row.kv -> $n, %cell {
                            next if %cell<no-cell>;
                            my $data = %cell<data>.Str.trim;
                            my $chars = duospace-width($data);
                            my $col-wid = @col-wids[$n];
                            if %cell<span>:exists {
                                #for the col-span
                                if %cell<span>[0] > 1 {
                                    for ^( %cell<span>[0] - 1) {
                                        $col-wid += @col-wids[ $n + $_ + 1] + 2
                                    }
                                }
                                #for the row-span
                                if %cell<span>[1] > 1 {
                                    for ^ (%cell<span>[1] - 1 ) {
                                        @rendered-grid[$r + $_ + 1][$n] ~= ' ' x $col-wid ~ ' |'
                                    }
                                }
                            }
                            my $pref = ( $col-wid - $chars ) div 2;
                            my $post = $col-wid - $pref - $chars;
                            @rendered-grid[ $r ][ $n ] ~=
                                ' ' x $pref ~
                                (%cell<header> || %cell<label> ?? BOLD-ON !! '') ~
                                $data ~
                                (%cell<header> || %cell<label> ?? BOLD-OFF !! '')
                                ~ ' ' x $post ~ ' |';
                        }
                    }
                    my $cap-shift = ( $table-wid - $cap-width ) div 2;
                    my $row-shift = $cap-shift <= 0 ?? - $cap-shift !! 0;
                    $cap-shift = 0 if $cap-shift <= 0;
                    PStr.new: $del ~
                        "\n" ~ ' ' x $cap-shift ~ $caption ~"\n" ~
                        @rendered-grid.map({
                        ' ' x $row-shift ~ '| ' ~ $_.grep( *.isa(Str) ).join('') ~ "\n"
                        }).join('') ~ "\n\n"
                   ;
                }
                else {
                    my $headers = '';
                    my $cap-shift = 0;
                    if %prm<headers>:exists && %prm<headers>[0] {
                        my @headers := %prm<headers>[0];
                        $cap-shift = (([+] @headers>>.Str>>.chars) + (3 * +@headers) + 4 - $cap-width ) div 2;
                        $headers = '| ' ~ BOLD-ON ~ %prm<headers>[0].join( BOLD-OFF ~ ' | ' ~ BOLD-ON ) ~ BOLD-OFF ~ " |\n"
                    }
                    my $row-shift = $cap-shift <= 0 ?? - $cap-shift !! 0;
                    $headers = ' ' x $row-shift ~ $headers if $headers;
                    $cap-shift = 0 if $cap-shift <= 0;
                    PStr.new: $del ~
                        ' ' x $cap-shift ~
                        $caption ~ "\n" ~
                        $headers ~
                        %prm<rows>.map({
                            ' ' x $row-shift ~
                            '| ' ~ $_.join(' | ') ~ " |\n"
                        }).join('') ~ "\n\n"
                }
            },
            #| renders any unknown block minimally
            unknown => -> %prm, $tmpl {
                PStr.new: